-
Notifications
You must be signed in to change notification settings - Fork 678
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
bugfix: do not render things when no items are in cart #2112
bugfix: do not render things when no items are in cart #2112
Conversation
… or price summary when no items are in cart Signed-off-by: Stephen Rugh <rugh@adobe.com>
|
@sirugh while this functions as it is supposed to, I think some addition finesse could be given to the 'fading out' of elements. |
|
||
export const CartPageFragment = gql` | ||
fragment CartPageFragment on Cart { | ||
total_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.
Should this fragment just compose itself of the other block components ie:
export const CartPageFragment = gql`
fragment CartPageFragment on Cart {
total_quantity
...PriceSummaryFragment
...PriceAdjustmentsFragment
...ProductListingFragment
}
`
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.
And then in mutations, instead of requesting individual fragments we could just request the CartPageFragment
?
export const REMOVE_ITEM_MUTATION = gql`
mutation removeItem($cartId: String!, $itemId: Int!) {
removeItemFromCart(input: { cart_id: $cartId, cart_item_id: $itemId }) {
cart {
id
...CartPageFragment
...PriceSummaryFragment
...PriceAdjustmentsFragment
...ProductListingFragment
}
}
}
`;
vs
export const REMOVE_ITEM_MUTATION = gql`
mutation removeItem($cartId: String!, $itemId: Int!) {
removeItemFromCart(input: { cart_id: $cartId, cart_item_id: $itemId }) {
cart {
id
...CartPageFragment
}
}
}
`;
The former approach is more "slim" in that it requests specific data whereas the latter just does a bulk fetch on mutate.
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 like the idea of an encapsulating page fragment, which future proofs new features. When new data is needed on the page, a new fragment is added to the page fragment, and any existing mutations automatically pick it up. Easier to maintain, but also relies on the assumption that over fetching doesn't cause unnecessary renders.
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.
Seems like over fetching would only cause re-renders if the data changes.
Signed-off-by: Stephen Rugh <rugh@adobe.com>
Signed-off-by: Stephen Rugh <rugh@adobe.com>
@@ -95,7 +92,9 @@ export const REMOVE_ITEM_MUTATION = gql` | |||
removeItemFromCart(input: { cart_id: $cartId, cart_item_id: $itemId }) { | |||
cart { | |||
id | |||
...CartPageFragment |
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.
When performing a mutation within the CartPage
we can just refetch the entire composed fragment rather than requiring each component to fetch specifics. This may result in "overfetch" such as in the case of adding a coupon where you only really need to update the total and discounts.
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 fine with this, under the assumption that over fetching doesn't causes unnecessary re-renders if that data didn't change.
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'll have to look -- I'm not sure, it seems super fast.
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.
For now I left the fetch policy so this doesn't matter.
id | ||
items { | ||
id | ||
product { |
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 props requested in items should probably be moved to a productListingProductFragment
object that is then used here. The idea is that each visual component declares what it needs to render, and the productListing
component is really just a wrapper that passes the data down to the product
component anyways.
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 like this idea, a great extension of what you did with PriceSummary. This did bring to my attention we're inconsistently colocating fragments though, and should probably decide on a single location.
- Dedicated file (
<componentName>Fragments.js
) - Named export in component (eg.
discountSummary
)
Of course the Apollo docs use static properties, which we know has the issue of cyclical dependencies when children need parent fragments (also an issue with named export). Maybe this is another reason to switch to a single root component query, and use apollo-anywhere filter. This still would require CartPageFragment
be exported outside of the root component though...
Tough questions, but I'm okay iterating on it and not solving in the scope of this PR.
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.
Yea I just wanted to call it out :)
@@ -37,11 +42,11 @@ const CartPage = props => { | |||
<ProductListing /> | |||
</div> | |||
<div className={classes.price_adjustments_container}> | |||
<PriceAdjustments /> | |||
{hasItems ? <PriceAdjustments /> : null} |
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.
Should we do the same here for ProductListing
? That component (and PriceSummary
, actually) have a null
render state based on the data they receive back.
If we add a conditional before rendering though, we prevent the component from mounting/making a graphQL query, which could be a win. Thoughts?
@@ -1,5 +1,6 @@ | |||
query getCartDetails($cartId: String!) { | |||
cart(cart_id: $cartId) { | |||
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 was missing, and potentially might cause cache misses.
However, in order for the trigger to be updated after mutations like removeItem
we would have to start using named fragments in this query. For now we can just refetch
.
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.
These cart queries use different versions of schema, so with the inclusion of id heuristic fragment matching wouldn't hit (must be exact). Even if we switched to consuming fragments here, we'd have to update the components to the new schema, which is making the scope creepy.
If this was a priority, I would probably just do a manual cache update in our mutations.
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.
As we discovered just now this works because Apollo sees "partial data" and thus makes a network call.
@schensley I definitely think it can be touched up, just not in the scope of this ticket. We should make a follow-up. Can you start that conversation with Andrew as it'll be good to get some mocks that show transition requirements as in what order things disappear and the exact timing of each? |
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.
Minor tweaks, and replies to your callouts. Looks good, should be a huge improvement.
@@ -95,7 +92,9 @@ export const REMOVE_ITEM_MUTATION = gql` | |||
removeItemFromCart(input: { cart_id: $cartId, cart_item_id: $itemId }) { | |||
cart { | |||
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.
Don't need this id stub anymore since you're using the fragment.
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.
So fragments should always include it? This was one of those things I wasn't sure about. Do we just always make sure to have some identifier for caching in our queries/mutations or can we assume that fragment authors will include it?
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.
Yes, I think we should make this assumption that all fragments include id when available. Ad-hoc inclusion of id would imply we're relying on heuristic fragment matching, which requires exact field matches, which is difficult to maintain. It's not causing any harm, but could lead to confusion that you always need to add an id field, when you don't.
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.
Do you still feel this way? I kind of prefer that we let fragments only define the data they need.
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 don't think fragments should need any additional config besides inclusion to work; without id in the fragment, developers would need to be aware they need it for cache updates to work.
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 makes sense to me that the base "wrapping" query would include the id
field, not the fragments.
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 seems like it could go either way -- either way we need to include id
somewhere and it does not hurt to declare it multiple times so I'll just leave it as is.
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.
Great compromise 🤝
@@ -95,7 +92,9 @@ export const REMOVE_ITEM_MUTATION = gql` | |||
removeItemFromCart(input: { cart_id: $cartId, cart_item_id: $itemId }) { | |||
cart { | |||
id | |||
...CartPageFragment |
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 fine with this, under the assumption that over fetching doesn't causes unnecessary re-renders if that data didn't change.
packages/venia-ui/lib/components/CartPage/ProductListing/productListing.js
Outdated
Show resolved
Hide resolved
@@ -8,9 +9,13 @@ import PriceSummary from './PriceSummary'; | |||
import ProductListing from './ProductListing'; | |||
import { mergeClasses } from '../../classify'; | |||
import defaultClasses from './cartPage.css'; | |||
import { CartPageFragment } from './cartPageFragments'; |
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 would support this overfetch if it prevented network calls in sub-components OR if you drilled the data down. Via chat you confirmed the first is not true; lets use a slimmer fragment that only grabs the data this component needs (total_quantity).
|
||
export const CartPageFragment = gql` | ||
fragment CartPageFragment on Cart { | ||
total_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.
I like the idea of an encapsulating page fragment, which future proofs new features. When new data is needed on the page, a new fragment is added to the page fragment, and any existing mutations automatically pick it up. Easier to maintain, but also relies on the assumption that over fetching doesn't cause unnecessary renders.
…art queries to match cache entries Signed-off-by: Stephen Rugh <rugh@adobe.com>
Signed-off-by: Stephen Rugh <rugh@adobe.com>
Signed-off-by: Stephen Rugh <rugh@adobe.com>
…d that's out of scope... Signed-off-by: Stephen Rugh <rugh@adobe.com>
Signed-off-by: Stephen Rugh <rugh@adobe.com>
Signed-off-by: Stephen Rugh <rugh@adobe.com>
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.
Death to the refetch! All hail fragments. Nice work, good groundwork to start iterating on this pattern.
Description
We were rendering the
PriceAdjustments
andPriceSummary
components regardless of whether we should.This PR adds a
getCartDetails
query that does an initial fetch for cart information, includingtotal_quantity
which it then uses to conditionally render the subcomponents.This PR also removes the
refetchQueries
from theremoveItem
mutation. It now uses named fragments! This means that after the remove item mutation is called you should see no subsequent refetches for new data, and yet the UI should update!Related Issue
Closes PWA-326.
Acceptance
Verification Stakeholders
@schensley @jimbo
Specification
Verification Steps
/cart
. You should see no items, no adjustments, and no summary.Screenshots / Screen Captures (if appropriate)
Checklist