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

Add the useOptimisticCart hook #2069

Merged
merged 11 commits into from
May 15, 2024
Merged

Add the useOptimisticCart hook #2069

merged 11 commits into from
May 15, 2024

Conversation

blittle
Copy link
Contributor

@blittle blittle commented May 1, 2024

WHY are these changes introduced?

An optimistic cart makes cart actions immediately render in the browser while the action syncs to the server. This increases the perceived performance of the application.

WHAT is this pull request doing?

This PR adds the useOptimisticCart() hook. This hook takes the cart object as a parameter, and processes all pending cart actions, locally mutating the cart with optimistic state. Once the pending cart actions resolve, cart object should automatically be updated from the server, and useOptimisticCart() just no-ops.

See the changeset for a deeper explanation and example.

HOW to test your changes?

  1. Load the skeleton template
  2. Add items to the cart
  3. Remove items from the cart
  4. Adjust the quantity of items in the cart

Post-merge steps

Checklist

  • I've read the Contributing Guidelines
  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've added a changeset if this PR contains user-facing or noteworthy changes
  • I've added tests to cover my changes
  • I've added or updated the documentation
Copy link
Contributor

shopify bot commented May 1, 2024

Oxygen deployed a preview of your bl-optimistic-cart branch. Details:

Storefront Status Preview link Deployment details Last update (UTC)
Skeleton (skeleton.hydrogen.shop) ✅ Successful (Logs) Preview deployment Inspect deployment May 15, 2024 9:06 PM
vite ✅ Successful (Logs) Preview deployment Inspect deployment May 15, 2024 9:06 PM
custom-cart-method ✅ Successful (Logs) Preview deployment Inspect deployment May 15, 2024 9:06 PM
subscriptions ✅ Successful (Logs) Preview deployment Inspect deployment May 15, 2024 9:06 PM
third-party-queries-caching ✅ Successful (Logs) Preview deployment Inspect deployment May 15, 2024 9:06 PM
optimistic-cart-ui ✅ Successful (Logs) Preview deployment Inspect deployment May 15, 2024 9:06 PM

Learn more about Hydrogen's GitHub integration.

@@ -67,16 +69,25 @@ type CartDiscountCodesUpdateRequire = {
} & OtherFormData;
};

export type OptimisticCartLine = CartLineInput & {
selectedVariant?: unknown;
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'm trying to figure out the right type to set here. It's tricky. ProductVariant does not work.

@blittle blittle force-pushed the bl-optimistic-cart branch 2 times, most recently from 582bb03 to 9789710 Compare May 2, 2024 14:14
import type {CartLineUpdateInput} from '@shopify/hydrogen/storefront-api-types';
import {Link} from '@remix-run/react';
import type {CartApiQueryFragment} from 'storefrontapi.generated';
import {useVariantUrl} from '~/lib/variants';

type CartLine = CartApiQueryFragment['lines']['nodes'][0];
type CartLine = OptimisticCart<CartApiQueryFragment>['lines']['nodes'][0];
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 OptimisticCart type augments the normal cart fragment to include the isOptimistic properties.

@@ -184,7 +208,7 @@ function CartLineQuantity({line}: {line: CartLine}) {
<CartLineUpdateButton lines={[{id: lineId, quantity: prevQuantity}]}>
<button
aria-label="Decrease quantity"
disabled={quantity <= 1}
disabled={quantity <= 1 || !!isOptimistic}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disable the cart line action buttons when line.isOptimistic is true. isOptimistic only goes to true when it's an entirely new line. So a new item in the cart that hasn't synced yet to the server. Actions cannot be taken because there is no lineId yet. But other optimistic actions, like changing the line quantity with LinesUpdate, line.isOptimistic stays false. This allows the user to rapidly click the +1 button to add many to the cart. The root cart.isOptimistic does get set to true. So the dev could still show a generic visual indicator for the whole cart until all actions are resolved.

Copy link
Contributor

@juanpprieto juanpprieto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this, great work @blittle.

Side note:

I noticed a layout shift because the price is not optimistic. I would maybe add adding or $0 to prevent the vertical shift

cart-layout-shift.mp4

Probably unrelated, but if you notice closely there's also a flash on the already rendered line item's image when the cart become non-optimistic.

@blittle
Copy link
Contributor Author

blittle commented May 8, 2024

@juanpprieto

I noticed a layout shift because the price is not optimistic. I would maybe add adding or $0 to prevent the vertical shift

Yeah I was wondering about this, there are a few problems with us setting a currency:

  1. I think it's an odd experience to see it costing zero dollars, then a moment later seeing a real cost. What about a really slow connection? What if it lasted a few seconds?
  2. The currency is a localized string from the server. I'd need to try and render a localized string in the browser, which would be tricky, and also likely not match what the server has.

Instead I think it's better to just use the line.isOptimistic flag to change how it renders, and use markup that doesn't cause a shift. I updated the skeleton template to not show the price, but also not shift the content when the price comes in.

Copy link
Contributor

@frandiox frandiox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works great Bret! And a really simple API 🔥 🚢


I was noticing that the images in the cart blink when requests are resolved but then realized it's because I was disabling browser cache. On React re-render it was probably downloading a different image even though the URL was the same. So no issue for us :)

const cartId = result.cart.id;
const headers = cart.setCartId(result.cart.id);
const cartId = result?.cart?.id;
const headers = cartId ? cart.setCartId(result.cart.id) : new Headers();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking maybe new Headers() would break SSR in Node.js... but it was added in v18 globally, which is our minimum required version 😄

packages/hydrogen/src/cart/queries/cartLinesAddDefault.ts Outdated Show resolved Hide resolved
packages/hydrogen/src/cart/queries/cartLinesAddDefault.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@juanpprieto juanpprieto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work Bret and thanks for incorporating the various feedbacks!

@blittle blittle merged commit fe82119 into main May 15, 2024
13 checks passed
@blittle blittle deleted the bl-optimistic-cart branch May 15, 2024 21:08
@blittle blittle restored the bl-optimistic-cart branch June 12, 2024 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
4 participants