Skip to content
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

Merged
merged 9 commits into from
Jan 23, 2020

Conversation

sirugh
Copy link
Contributor

@sirugh sirugh commented Jan 22, 2020

Description

We were rendering the PriceAdjustments and PriceSummary components regardless of whether we should.

This PR adds a getCartDetails query that does an initial fetch for cart information, including total_quantity which it then uses to conditionally render the subcomponents.

This PR also removes the refetchQueries from the removeItem 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

  1. Clear your storage and go to /cart. You should see no items, no adjustments, and no summary.
  2. Add a product and go back to `/cart. You should see the item, adjustments, and summary.
  3. Remove the item with the kebab toggle. The item, adjustments, and summary views should disappear.

Screenshots / Screen Captures (if appropriate)

Image from Gyazo

Checklist

  • I have updated the documentation accordingly, if necessary.
  • I have added tests to cover my changes, if necessary.

… or price summary when no items are in cart

Signed-off-by: Stephen Rugh <rugh@adobe.com>
@m2-community-project m2-community-project bot added this to Ready for Review in Pull Request Progress Jan 22, 2020
@sirugh sirugh added the version: Minor This changeset includes functionality added in a backwards compatible manner. label Jan 22, 2020
@PWAStudioBot
Copy link
Contributor

PWAStudioBot commented Jan 22, 2020

Messages
📖

Access a deployed version of this PR here. Make sure to wait for the "pwa-pull-request-deploy" job to complete.

📖 DangerCI Failures related to missing labels/description/linked issues/etc will persist until the next push or next nightly build run (assuming they are fixed).
📖

Associated JIRA tickets: PWA-326.

Generated by 🚫 dangerJS against f21e8e4

@schensley
Copy link

@sirugh while this functions as it is supposed to, I think some addition finesse could be given to the 'fading out' of elements.
Can this behave in such a way that first the removed product line fades out, and then the Pricing Summary and the accordion of modules (Shipping, Coupon, etc.) fades out after? It feels abrupt to have all fade out (at least so quickly). Thoughts?

@m2-community-project m2-community-project bot moved this from Ready for Review to Review in Progress in Pull Request Progress Jan 22, 2020

export const CartPageFragment = gql`
fragment CartPageFragment on Cart {
total_quantity
Copy link
Contributor Author

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
    }
`

Copy link
Contributor Author

@sirugh sirugh Jan 22, 2020

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 {
Copy link
Contributor Author

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.

Copy link
Contributor

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.

  1. Dedicated file (<componentName>Fragments.js)
  2. 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.

Copy link
Contributor Author

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}
Copy link
Contributor Author

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
Copy link
Contributor Author

@sirugh sirugh Jan 22, 2020

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@sirugh
Copy link
Contributor Author

sirugh commented Jan 22, 2020

I think some addition finesse could be given to the 'fading out' of elements. Can this behave in such a way that first the removed product line fades out, and then the Pricing Summary and the accordion of modules (Shipping, Coupon, etc.) fades out after? It feels abrupt to have all fade out (at least so quickly). Thoughts?

@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?

Copy link
Contributor

@tjwiebell tjwiebell left a 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
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor

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.

@@ -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';
Copy link
Contributor

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
Copy link
Contributor

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>
@m2-community-project m2-community-project bot moved this from Changes Requested to Review in Progress in Pull Request Progress Jan 23, 2020
Signed-off-by: Stephen Rugh <rugh@adobe.com>
Signed-off-by: Stephen Rugh <rugh@adobe.com>
Copy link
Contributor

@tjwiebell tjwiebell left a 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.

@dpatil-magento dpatil-magento merged commit 0d72488 into develop Jan 23, 2020
@dpatil-magento dpatil-magento deleted the rugh/cart-no-adjustments-without-items branch January 23, 2020 22:50
@m2-community-project m2-community-project bot moved this from Review in Progress to Done in Pull Request Progress Jan 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg:peregrine pkg:venia-ui version: Minor This changeset includes functionality added in a backwards compatible manner.
Development

Successfully merging this pull request may close these issues.

None yet

6 participants