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

Cannot call multiple useCart callbacks consecutively #685

Open
urgoringo opened this issue Mar 14, 2023 · 6 comments
Open

Cannot call multiple useCart callbacks consecutively #685

urgoringo opened this issue Mar 14, 2023 · 6 comments

Comments

@urgoringo
Copy link

What is the location of your example repository?

No response

Which package or tool is having this issue?

hydrogen-react

What version of that package or tool are you using?

2023.1.6

What version of Remix are you using?

No response

Steps to Reproduce

Hello. I'm trying to execute multiple operations in a sequence using the useCart hook e.g. cartAttributesUpdate and linesRemove.

However, it seems only the first operation gets executed. I also tried a solution where I'm "sleeping" until cart.status === "idle" before calling another callback. This doesn't work either for some reason.

Am I missing something or my use case is not supported by the library?

I can reproduce the error with following test:

import {act, renderHook, waitFor} from "@testing-library/react";
import React, {ReactNode} from "react";
import "whatwg-fetch";
import {CartProvider, ShopifyProvider, useCart} from "@shopify/hydrogen-react";

const SHOPIFY_DOMAIN = <your shopify domain>;
const STOREFRONT_ACCESS_TOKEN = <your storefront access token>;
// need to have this product available in your store
const aProductVariant = "gid://shopify/ProductVariant/41515070947428"

const renderCart = () => {
  const wrapper = ({children}: {children: ReactNode}) => (
    <ShopifyProvider
      storeDomain={`https://${SHOPIFY_DOMAIN}`}
      storefrontApiVersion="2023-01"
      storefrontToken={STOREFRONT_ACCESS_TOKEN}
      countryIsoCode="EE"
      languageIsoCode="ET"
    >
      <CartProvider
        onLineAddComplete={() => console.log("Line add complete")}
        onLineUpdateComplete={() => console.log("Line update complete")}
        onAttributesUpdateComplete={() =>
          console.log("Attributes update complete")
        }
      >
        {children}
      </CartProvider>
    </ShopifyProvider>
  );
  return renderHook(() => useCart(), {wrapper});
};

const sleepUntil = async (f: () => boolean, timeoutMs = 1000) => {
  return new Promise((resolve, reject) => {
    const timeWas = new Date();
    const wait = setInterval(function () {
      if (f()) {
        clearInterval(wait);
        resolve(true);
      } else if (+new Date() - +timeWas > timeoutMs) {
        clearInterval(wait);
        reject(false);
      }
    }, 20);
  });
};

test("can call multiple callbacks in sequence", async () => {
  const {result} = renderCart();

  act(() => result.current.cartCreate({}));
  await waitFor(() => {
    expect(result.current.status).toBe("idle");
  });
  await act(() => {
    result.current.linesAdd([
      {
        merchandiseId: aProductVariant,
        quantity: 1,
      },
    ]);
  });
  await waitFor(() => {
    expect(result.current.status).toBe("idle");
  });
  await act(() => {
    result.current.cartAttributesUpdate([
      {key: "test2", value: "val2"},
      {key: "test2", value: "val3"},
    ]);
    sleepUntil(() => result.current.status === "idle");
    //doing this inside setTimeout e.g. 800ms works
    result.current.linesRemove(result.current.lines.map((it) => it.id));
  });
  await waitFor(() => {
    expect(result.current.status).toBe("idle");
  });
  await waitFor(() => {
    expect(
      result.current.attributes.filter((it) => it.key === "test2")[0]
    ).toStrictEqual({
      key: "test2",
      value: "val3",
    });
  });
  //this fails for me 
  await waitFor(() => {
    expect(result.current.lines.length).toBe(0);
  });
});

Expected Behavior

When calling multiple callbacks in a row from my custom hook then both callbacks get executed. If it's not allowed to executed multiple updates in parallel then useCart API could offer a way to wait until the previous update has completed.

Actual Behavior

Only the first callback gets executed and the subsequent one is ignored.

@blittle
Copy link
Contributor

blittle commented Mar 15, 2023

Should you be await-ing your sleepUntil?

@urgoringo
Copy link
Author

Yes, I have that typo in my test :) . However, even when adding it this still happens. I created sample repository where test can reproduce this issue: https://github.com/urgoringo/test-shopify-storefront/

It seems that status is idle immediately after calling linesRemove. Only after linesAdd has been called as well I see that onLineRemoveComplete gets called.

Also, closed discussion about this to avoid duplication: #678

@lordofthecactus
Copy link
Contributor

Could you check something like this:

test("when calling line remove and line add the latter is ignored", async () => {
  const { result } = renderCart();

  act(() => result.current.cartCreate({}));
  await waitFor(() => {
    expect(result.current.status).toBe("idle");
  });
  await act(() => {
    result.current.linesAdd([
      {
        merchandiseId: "gid://shopify/ProductVariant/44671043141922",
        quantity: 1,
      },
    ]);
  });

  await waitFor(() => {
    console.log(result.current.status);
    expect(result.current.status).toBe("idle");
  });

  console.log("Going to remove line", new Date());
  await act(async () => {
    result.current.linesRemove(result.current.lines.map((it) => it.id));
  });

  await waitFor(() => {
    expect(result.current.status).toBe("idle");
  });

  await act(() => {
    result.current.linesAdd([
      {
        merchandiseId: "gid://shopify/ProductVariant/44671043141922",
        quantity: 20,
      },
    ]);
  });

  await waitFor(() => {
    expect(result.current.status).toBe("idle");
  });

  await waitFor(() => {
    expect(result.current.totalQuantity).toBe(20);
  });
});

from you repo, I saw you are having waitFor inside the act. The status will always stay idle within this act. Having waitFor outside will have the result we expect and will go through all the changes.

@urgoringo
Copy link
Author

urgoringo commented Mar 22, 2023

@lordofthecactus yes, indeed the test will then pass. However, in my case I want to call multiple useCart operations within a single method of my custom hook. More specifically, whenever some cart attributes are changed I need to make changes to cart lines as well.

I have updated the scenario in another test: https://github.com/urgoringo/test-shopify-storefront/blob/main/useCustomCart.test.tsx

Is there any way how to handle that?

Also, somewhat related issue is when I want to make some changes to cart just before I redirect user to checkout. How am I supposed to make sure that these last changes have been made successfully before doing the redirect to checkout page?

If all userCart mutations returned a Promise I could easily solve all these issues by just awaiting.

@Rahza
Copy link

Rahza commented Mar 22, 2023

What I ended up doing is writing another custom context component (as a child of the <CartProvider />) in which I then use useEffect() to listen for changes of cart lines and thus indirectly "chain" API calls. Depending on your use case, especially if you need to chain multiple API calls in a row, this will not work.

I agree that the functions exposed by useCart() should all return Promises for easy chaining of commands.

@urgoringo
Copy link
Author

Thanks you @Rahza! I used useEffect for some cases as well but wasn't sure if it was the best solution. Will go with that for now then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants