The cart's reservation countdown is frozen — it never ticks, and the expiry warning never appears #97

Closed
opened 2026-08-21 15:56:31 -05:00 by bermudalamb · 0 comments
Owner

Cart.tsx shows each held item as "2h 15m left" and turns the text red in the final hour. Neither updates. The number a customer reads is whatever it was at the moment the page rendered, and the warning colour appears only by accident.

Why

Both readings happen during render, from the wall clock, and nothing schedules a re-render:

// Cart.tsx:32
function timeRemaining(expiresAt: string): string {
  const diffMs = new Date(expiresAt).getTime() - Date.now();
  ...
}

// Cart.tsx:177
<Text type={new Date(item.expires_at).getTime() - Date.now() < 60 * 60 * 1000 ? 'danger' : 'secondary'}>
  {timeRemaining(item.expires_at)}
</Text>

There is no setInterval and no timer anywhere in the file — confirmed by grep, not assumption. React re-renders when state or props change, and neither does here, so the countdown is a still photograph of the moment the cart loaded.

react-hooks/purity flags exactly this and is currently the only instance in the codebase: "Cannot call impure function during render." It is easy to read that as a purity nitpick. It is not — the rule is pointing at a real defect, because a render whose output depends on the clock cannot be correct unless something re-runs it.

What a customer actually experiences

They open the cart with a held item and read "0h 14m left". They spend ten minutes choosing a shipping address. The text still reads "0h 14m left". The reservation expires, the item silently returns to the storefront, and the only warning the design provides — the red text in the final hour — was never going to appear, because the component had already rendered before the final hour began.

This matters more here than in most shops: every item is one of a kind, so a lapsed reservation is not "buy it later", it is someone else buying the only one.

Suggested fix

A single interval in Cart.tsx that re-renders while any item is held — a useState tick advanced every 30 seconds, cleared on unmount, and not started at all when the cart is empty so an idle cart page is not waking React forever.

Worth deciding:

  • Tick rate. The display has a one-minute resolution, so 30 seconds is enough to keep it honest without being busy. Once a second would be wasted work.
  • What happens at zero. timeRemaining already returns "expiring…" for a non-positive difference, but nothing removes the item or refreshes the cart. Deciding whether the page should refetch when an item lapses is part of fixing this properly — otherwise the row sits at "expiring…" forever, which is a quieter version of the same lie.
  • Whether the server agrees. Expiry is enforced by a sweep in server.ts every five minutes, so an item can read "expiring…" in the browser while still being held, or read time remaining while already swept. The display should not claim more precision than the sweep provides.

Verification

An end-to-end test can advance the countdown by seeding an item whose expires_at is close and asserting the text changes without any interaction — which fails today, because nothing re-renders.

Severity

Medium-high. Nothing is broken server-side and no data is wrong; the customer is simply shown a stopped clock during the one flow where time actually matters to them, and a warning that was designed and then cannot fire.

Found during a React best-practices review.

`Cart.tsx` shows each held item as "2h 15m left" and turns the text red in the final hour. Neither updates. The number a customer reads is whatever it was at the moment the page rendered, and the warning colour appears only by accident. ## Why Both readings happen during render, from the wall clock, and **nothing schedules a re-render**: ```tsx // Cart.tsx:32 function timeRemaining(expiresAt: string): string { const diffMs = new Date(expiresAt).getTime() - Date.now(); ... } // Cart.tsx:177 <Text type={new Date(item.expires_at).getTime() - Date.now() < 60 * 60 * 1000 ? 'danger' : 'secondary'}> {timeRemaining(item.expires_at)} </Text> ``` There is no `setInterval` and no timer anywhere in the file — confirmed by grep, not assumption. React re-renders when state or props change, and neither does here, so the countdown is a still photograph of the moment the cart loaded. `react-hooks/purity` flags exactly this and is currently the only instance in the codebase: *"Cannot call impure function during render."* It is easy to read that as a purity nitpick. It is not — the rule is pointing at a real defect, because a render whose output depends on the clock cannot be correct unless something re-runs it. ## What a customer actually experiences They open the cart with a held item and read "0h 14m left". They spend ten minutes choosing a shipping address. The text still reads "0h 14m left". The reservation expires, the item silently returns to the storefront, and the only warning the design provides — the red text in the final hour — was never going to appear, because the component had already rendered before the final hour began. This matters more here than in most shops: every item is one of a kind, so a lapsed reservation is not "buy it later", it is someone else buying the only one. ## Suggested fix A single interval in `Cart.tsx` that re-renders while any item is held — a `useState` tick advanced every 30 seconds, cleared on unmount, and not started at all when the cart is empty so an idle cart page is not waking React forever. Worth deciding: - **Tick rate.** The display has a one-minute resolution, so 30 seconds is enough to keep it honest without being busy. Once a second would be wasted work. - **What happens at zero.** `timeRemaining` already returns "expiring…" for a non-positive difference, but nothing removes the item or refreshes the cart. Deciding whether the page should refetch when an item lapses is part of fixing this properly — otherwise the row sits at "expiring…" forever, which is a quieter version of the same lie. - **Whether the server agrees.** Expiry is enforced by a sweep in `server.ts` every five minutes, so an item can read "expiring…" in the browser while still being held, or read time remaining while already swept. The display should not claim more precision than the sweep provides. ## Verification An end-to-end test can advance the countdown by seeding an item whose `expires_at` is close and asserting the text changes without any interaction — which fails today, because nothing re-renders. ## Severity Medium-high. Nothing is broken server-side and no data is wrong; the customer is simply shown a stopped clock during the one flow where time actually matters to them, and a warning that was designed and then cannot fire. Found during a React best-practices review.
bermudalamb added this to the Code Quality and Hardening 2 project 2026-08-21 16:08:37 -05:00
bermudalamb self-assigned this 2026-08-21 16:09:01 -05:00
bermudalamb added reference feature/97-cart-countdown 2026-08-24 09:02:51 -05:00
bermudalamb moved this to Review in Code Quality and Hardening 2 on 2026-08-24 09:03:03 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#97