-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Account's wishlist: integrate hooks #1042
Account's wishlist: integrate hooks #1042
Conversation
Because we're already showing the loading spinner anyways.
packages/commerce-sdk-react/src/hooks/ShopperCustomers/cache.ts
Outdated
Show resolved
Hide resolved
const clone: ShopperCustomersTypes.CustomerProductListResult = JSON.parse( | ||
JSON.stringify(oldData) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON.parse has performance issues... Does the suggestion below work for typescript?
const clone: ShopperCustomersTypes.CustomerProductListResult = JSON.parse( | |
JSON.stringify(oldData) | |
) | |
const clone: ShopperCustomersTypes.CustomerProductListResult = {...oldData} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had {...oldData}
before, but it was just a shallow clone. I wanted a deep clone, since the property to update is nested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the most concise (but maybe not the most readable) way to implement this logic. General idea is "Shallow clone the object, deep clone the array, but replace the array item with matching ID" for both the list of lists and for the list of items.
return {
...oldData,
data: oldData.data.map((list) =>
list.id !== listId
? list
: {
...list,
customerProductListItems:
list.customerProductListItems?.map((item) =>
item.id !== itemId ? item : response
)
}
)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️ Thanks Will! Not only it's more concise, but I think it's more readable too.
const clone: ShopperCustomersTypes.CustomerProductListResult = JSON.parse( | ||
JSON.stringify(oldData) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the most concise (but maybe not the most readable) way to implement this logic. General idea is "Shallow clone the object, deep clone the array, but replace the array item with matching ID" for both the list of lists and for the list of items.
return {
...oldData,
data: oldData.data.map((list) =>
list.id !== listId
? list
: {
...list,
customerProductListItems:
list.customerProductListItems?.map((item) =>
item.id !== itemId ? item : response
)
}
)
}
// TODO: make separate PR that merges into develop | ||
{ | ||
// Also selectively update the lists | ||
queryKey: getCustomerProductLists.queryKey(parameters), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget to remove the query from the invalidate list; invalidate takes precedence over update. (Technically, it will update then invalidate, which negates the point of updating.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, after the list item is updated, the product lists become somewhat stale. For example, the lastModified
time is no longer true.
So I prefer to also invalidate the product lists. Yes, most of the data can be redundant, but the refetch is going to be done in the background. In some way, I think of it as similar to stale-while-revalidate strategy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we update and invalidate at the same time, does react query ever show the updated data? I thought that it just keeps the old data until the refetch completes. (I could very easily be wrong.)
Also, I just noticed that we update/invalidate the lists endpoint, but we just invalidate the list endpoint. We should follow the same pattern for both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You brought up good points there. To make things simpler, I've reverted the cache-update logic.
This article mentions that it's safer and simpler to just invalidate, rather than update. And I can see that here in our case.
So what now? There's actually another way to achieve what I wanted: via local state update in the quantity picker component. Take a look at this commit for details: 487c775
Thanks Will!
|
||
const addToCartButton = getByRole('button', { | ||
const addToCartButton = await screen.findByRole('button', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice await!
toast({title: formatMessage(API_ERROR_MESSAGE), status: 'error'}) | ||
}, | ||
onSettled: () => { | ||
onClick('') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why? I see that we previously do this but don't understand why, maybe it's worth adding a comment on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's so the callee can know when something has ended (and give you a chance to remove the loading spinner, for example). I've updated the code so it's clearer now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure if i understand, it's really weird to the caller that when user clicks the button, onClick
actually fires twice!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 maybe instead of onClick
prop, we have 2 props?
onActionStart
onActionEnd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kevinxh All right, I've converted the onClick
into those 2 events to be clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, i just saw this now... You could actually make onClick a promise.
const removeItemMock = jest.fn() | ||
renderWithProviders(<MockedComponent onClick={removeItemMock} />) | ||
|
||
const removeButton = await screen.findByRole('button', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
more awaits! good stuff!
onClick('') | ||
} | ||
const handleItemRemove = async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async
with no await
?
|
||
const handleActionClicked = (itemId) => { | ||
setWishlistItemLoading(!!itemId) | ||
setWishlistItemLoading(Boolean(itemId)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏
|
||
const {data: productsData, isLoading: isProductsLoading} = useProducts( | ||
{parameters: {ids: productIds?.join(','), allImages: true}}, | ||
{enabled: Boolean(productIds)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will send a request if productIds
is an empty array. Is that desired behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I'll update it to check for an array and that it's not empty.
// TODO: make separate PR that merges into develop | ||
{ | ||
// Also selectively update the lists | ||
queryKey: getCustomerProductLists.queryKey(parameters), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we update and invalidate at the same time, does react query ever show the updated data? I thought that it just keeps the old data until the refetch completes. (I could very easily be wrong.)
Also, I just noticed that we update/invalidate the lists endpoint, but we just invalidate the list endpoint. We should follow the same pattern for both.
quantity: localQuantity[item.productId] | ||
? localQuantity[item.productId] | ||
: item.quantity | ||
quantity: item.quantity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code in develop
removes the item from the wishlist when the shopper decrements the product quantity < 1, when taking either of these two actions in the QuantityPicker:
- Typing the number '0' in the quantity Input field.
- Pressing the '-' button when the quantity is 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, the smart deletion logic originally comes from:
pwa-kit/packages/template-retail-react-app/app/commerce-api/hooks/useCustomerProductLists.js
Lines 204 to 210 in 487c775
updateListItem: async (listId, item) => { | |
const {id, quantity} = item | |
if (quantity === 0) { | |
await removeListItem(listId, id) | |
actions.removeListItem(listId, id) | |
return | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI I've implemented that.
import {useToast} from '../../../hooks/use-toast' | ||
import {useWishList} from '../../../hooks/use-wish-list' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On develop
when the shopper clicks the "Add to Wishlist" button on one of the items of a product set, the individual item is added to the wishlist, whereas now the entire set is added to the wishlist instead of the individual product.
Example product set:
https://pwa-kit.mobify-storefront.com/global/en-GB/product/fall-lookM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm strange, that might be a regression after resolving merge conflicts.. let me investigate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I've fixed this issue.
But still achieve the same result via local state update within the component.
Bring back this original feature.
You can add the parent or child items to the wishlist.
? updateCustomerProductListItem.mutateAsync({body, parameters}) | ||
: deleteCustomerProductListItem.mutateAsync({parameters}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if you do update
with 0? Feels like the back end should be capable of handling the deletion logic when quantity is 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would happen is the back end would accept 0 as the minimum quantity, but it does not remove the item from the list.
onActionStart = noop, | ||
onActionEnd = noop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, if onClick
is a promise, then the consumer will know when the action ends. An example in our code base is the action card component's onRemove prop
@@ -129,7 +129,7 @@ const ProductDetail = () => { | |||
body: { | |||
// NOTE: APi does not respect quantity, it always adds 1 | |||
quantity, | |||
productId: urlParams.get('pid') || productId, | |||
productId: variant?.productId || product?.id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bug fix for making sure that adding to wishlist works correctly for a product set: you can add the parent item to the wishlist or the individual child items.
Progress so far:
For editing a wishlist item: tweak the cache-update logic so it also selectively updates the entire lists dataWhat's next:
Types of Changes
Changes
How to Test-Drive This PR
Checklists
General
Accessibility Compliance
You must check off all items in one of the follow two lists:
or...
Localization