Skip to content

useOrderSubmit never actually waits for approval confirmation (stale closure over isWaitingForTx) #1

@thegoodentity

Description

@thegoodentity

Where

examples/widget/src/lib/hooks/useOrderSubmit.ts:

await new Promise<void>((resolve) => {
const checkTx = setInterval(() => {
if (!isWaitingForTx) {
clearInterval(checkTx);
resolve();
}
}, 1000);
});

After sending the approval transaction, the hook tries to wait for confirmation before signing/submitting the order:

const { isLoading: isWaitingForTx } = useWaitForTransactionReceipt({ hash: txHash });
...
setTxHash(hash);

// Wait for transaction confirmation
await new Promise<void>((resolve) => {
  const checkTx = setInterval(() => {
    if (!isWaitingForTx) {
      clearInterval(checkTx);
      resolve();
    }
  }, 1000);
});

Problem

isWaitingForTx is captured by the running submit() closure. When submit() is invoked, txHash is still undefined, so the receipt query is disabled and isWaitingForTx is false:

  • wagmi useWaitForTransactionReceipt sets enabled = Boolean(hash && (query.enabled ?? true)), so with hash === undefined the query is disabled.
  • For a disabled query, TanStack returns isLoading = isPending && isFetching, and isFetching is false (fetchStatus is idle), so isLoading (i.e. isWaitingForTx) is false.

The setInterval callback keeps reading that stale false (a later re-render creates a new closure, but the already-running invocation never sees it), so the promise resolves on the very first tick (~1s) regardless of whether the approval tx has been mined. Signing and submitting the order then proceeds before the approval is confirmed on-chain.

(Conversely, if the captured value were ever true, the loop would never resolve and submit() would hang. So the wait is broken either way.) This is standard JS closure behavior and does not depend on the wagmi version.

Suggested fix

Await the receipt directly instead of polling a captured React value, for example with the viem public client:

import { usePublicClient } from "wagmi";
// ...
const publicClient = usePublicClient();
// ...
if (approvalAction) {
  setStep("approval");
  const hash = await sendTransactionAsync({ /* ... */ });
  setTxHash(hash);
  await publicClient.waitForTransactionReceipt({ hash }); // actually waits
}

Alternatively, drive the next step from a useEffect on the hook's isSuccess. Either approach removes the stale-closure polling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions