fix: prevent infinite transaction loading loop in WalletViewer

Previously, the transaction list would try to load constantly and fail
repeatedly due to a circular dependency in the useEffect hooks. The
listTransactions function from useWallet wasn't wrapped in useCallback,
causing loadInitialTransactions to be recreated on every render, which
triggered the useEffect infinitely.

Changes:
- Add txLoadAttempted and txLoadFailed flags to prevent repeated attempts
- Only attempt to load transactions once on wallet connection
- Fail silently on initial load (no toast spam)
- Show retry button when transaction loading fails
- Reset flags when wallet connects/disconnects or after successful payments
- Make transaction list truly optional - wallet still works if loading fails

This ensures a better UX when wallets don't support list_transactions
or when the method fails for any reason.
This commit is contained in:
Claude
2026-01-18 17:16:35 +00:00
parent d58c9b699f
commit 145048df39

View File

@@ -206,6 +206,8 @@ export default function WalletViewer() {
const [hasMore, setHasMore] = useState(true);
const [connectDialogOpen, setConnectDialogOpen] = useState(false);
const [disconnectDialogOpen, setDisconnectDialogOpen] = useState(false);
const [txLoadAttempted, setTxLoadAttempted] = useState(false);
const [txLoadFailed, setTxLoadFailed] = useState(false);
// Send dialog state
const [sendDialogOpen, setSendDialogOpen] = useState(false);
@@ -245,7 +247,11 @@ export default function WalletViewer() {
}, [getInfo]);
const loadInitialTransactions = useCallback(async () => {
// Prevent repeated attempts if already failed
if (txLoadFailed) return;
setLoading(true);
setTxLoadAttempted(true);
try {
const result = await listTransactions({
limit: BATCH_SIZE,
@@ -254,26 +260,37 @@ export default function WalletViewer() {
const txs = result.transactions || [];
setTransactions(txs);
setHasMore(txs.length === BATCH_SIZE);
setTxLoadFailed(false);
} catch (error) {
console.error("Failed to load transactions:", error);
toast.error("Failed to load transactions");
setTxLoadFailed(true);
// Don't show toast on initial load - just fail silently
// User can retry via refresh button if needed
} finally {
setLoading(false);
}
}, [listTransactions]);
}, [listTransactions, txLoadFailed]);
useEffect(() => {
if (isConnected) {
loadWalletInfo();
// Reset transaction loading flags when wallet connects
setTxLoadAttempted(false);
setTxLoadFailed(false);
setTransactions([]);
}
}, [isConnected, loadWalletInfo]);
// Load transactions when wallet info is available
// Load transactions when wallet info is available (only once)
useEffect(() => {
if (walletInfo?.methods.includes("list_transactions")) {
if (
walletInfo?.methods.includes("list_transactions") &&
!txLoadAttempted &&
!loading
) {
loadInitialTransactions();
}
}, [walletInfo, loadInitialTransactions]);
}, [walletInfo, txLoadAttempted, loading, loadInitialTransactions]);
useEffect(() => {
if (!generatedPaymentHash || !receiveDialogOpen) return;
@@ -288,6 +305,9 @@ export default function WalletViewer() {
toast.success("Payment received!");
setReceiveDialogOpen(false);
resetReceiveDialog();
// Reload transactions - reset flags to allow reload
setTxLoadAttempted(false);
setTxLoadFailed(false);
loadInitialTransactions();
}
} catch (error) {
@@ -491,7 +511,9 @@ export default function WalletViewer() {
toast.success("Payment sent successfully");
resetSendDialog();
setSendDialogOpen(false);
// Reload transactions
// Reload transactions - reset flags to allow reload
setTxLoadAttempted(false);
setTxLoadFailed(false);
loadInitialTransactions();
} catch (error) {
console.error("Payment failed:", error);
@@ -823,6 +845,24 @@ export default function WalletViewer() {
<div className="flex h-full items-center justify-center">
<RefreshCw className="size-6 animate-spin text-muted-foreground" />
</div>
) : txLoadFailed ? (
<div className="flex h-full flex-col items-center justify-center gap-3 p-4">
<p className="text-sm text-muted-foreground text-center">
Failed to load transaction history
</p>
<Button
variant="outline"
size="sm"
onClick={() => {
setTxLoadFailed(false);
setTxLoadAttempted(false);
loadInitialTransactions();
}}
>
<RefreshCw className="mr-2 size-4" />
Retry
</Button>
</div>
) : transactionsWithMarkers.length === 0 ? (
<div className="flex h-full items-center justify-center">
<p className="text-sm text-muted-foreground">