Shopify / ui-extensions

MIT License
268 stars 36 forks source link

[Checkout UI Extension] `useCartlines()` multiple emits on page load #1918

Open ctrlaltdylan opened 6 months ago

ctrlaltdylan commented 6 months ago

Please list the package(s) involved in the issue, and include the version you are using

    "@shopify/ui-extensions": "2024.1.x",
    "@shopify/ui-extensions-react": "2024.1.x",

Describe the bug

Steps to reproduce the behavior:

  1. Set up a checkout UI extension that implements useCartLines()
  2. Set up a useEffect that depends on cartLines from useCartLines()
  3. Observe two emits from useCartLines on every page load

Expected behavior

Like the other cart hooks, I expect useCartLines() to only emit a single event on cart changes. But it emits twice.

Screenshots

Additional context

  const cartLines = useCartLines();

useEffect(() => {
  console.log(`Cart lines changed`);
}, [cartLines])
milesmcleod commented 6 months ago

I have also observed this recently. Would be great to get it fixed.

@ctrlaltdylan My workaround was to write a custom hook that effectively wraps the lines from const { lines } = useApi<'purchase.checkout.block.render'>() like so, so that i can use my own returned state value that relies on lodash isEquals as a dependency in other hooks, rather than whatever is used under the hood that causes the extra, unnecessary re-renders:

import { CartLine, StandardApi } from '@shopify/ui-extensions/checkout'
import { useState } from 'react'
import { isEqual } from 'lodash'

/**
 * This hook returns the current cart lines. Use it instead
 * of the Shopify-provided useCartLines hook to eliminate shopify rerender bugs, specifically
 * because the array equality method used under the hood by Shopify when checking for cart line
 * diffs seems to sometimes returns false positives that may cause unnecessary re-renders. This wrapper uses
 * lodash's isEqual method to compare the cart lines and this method seems more reliable.
 *
 * @param shopifyLines {@link StandardApi<'purchase.checkout.block.render'>['lines']}
 * @returns the current cart lines
 */
export function useCartLines(
  shopifyLines: StandardApi<'purchase.checkout.block.render'>['lines'],
): CartLine[] {
  // set up state values
  const [cartLines, setCartLines] = useState<CartLine[]>([])
  // use the remote ui StatefulRemoteSubscribable api to subscribe to cart line changes,
  // and update the state with the new lines when they have actually changed.
  shopifyLines.subscribe((newLines: CartLine[]) => {
    if (!isEqual(newLines, cartLines)) {
      setCartLines(newLines)
    }
  })
  return cartLines
}

Not sure if it'll work for you but I figured I would share!

jamesvidler commented 6 months ago

Thank you for reporting this issue. We've reproduced it and will look to resolve this in a future update.

@milesmcleod Have you considered how importing lodash into your extension increases the bundle size? I wonder if there could be a slimmer way to do a comparison in this case?