Files
mercadodevida/work/artifacts/F-063/implementer.md
2026-08-19 17:07:45 +02:00

3.1 KiB

F-063 — Implementer evidence

Scope delivered

The product editor in apps/admin/src/features/products/components/ProductEditor.tsx called productsApi.get(productId) on every render because the load effect had getSnap (a useCallback rebuilt whenever any of the form state changes) in its dependency list. Combined with setCategoryIds(p.categoryIds ?? []) producing a fresh array reference on every load, React never bailed out, the effect re-fired, and the page settled into an infinite fetch loop. The visible symptom was a hanging GET /api/products/:id and the inability to interact with the featured toggle, the category list, and the attribute grid — every click was immediately overwritten by the next fetch.

Change

project/apps/admin/src/features/products/components/ProductEditor.tsx

  • Removed getSnap from the load effect's dependency list. The effect now runs once per productId (a one-shot mount), with an explicit eslint-disable-next-line react-hooks/exhaustive-deps and a cancelled flag to drop late responses from a stale request.
  • Introduced a getSnapRef that is updated each render. The load handler now uses getSnapRef.current() to compute the initial snapshot, so the load effect no longer depends on a function that changes with form state.
  • The dirty-check effect keeps getSnap in its deps — that one is intentionally re-evaluated on every form change and does not cause a re-fetch.
const getSnapRef = useRef(getSnap);
useEffect(() => { getSnapRef.current = getSnap; }, [getSnap]);

useEffect(() => {
  if (!productId) { setLoading(false); return; }
  let cancelled = false;
  productsApi.get(productId).then((p: Product) => {
    if (cancelled) return;
    setName(p.name); setSlug(p.slug); setDesc(p.description ?? '');
    // …
    snapRef.current = getSnapRef.current();
    setLoading(false);
  }).catch(() => {
    if (cancelled) return;
    setError('No se pudo cargar el producto'); setLoading(false);
  });
  return () => { cancelled = true; };
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [productId]);

Acceptance traceability

Acceptance criterion How it is met
GET /api/products/:id fires exactly once on mount and again only on navigation The load effect depends only on productId; cancelled guard drops stale responses.
Clicking featured and a category updates state and persists after re-render With the loop gone, setFeatured / setCategoryIds actually apply and stick.
Loading spinner disappears after the first successful fetch and never reappears setLoading(false) only runs inside the load effect, which no longer re-fires.
verify.sh is green Exit 0.

Manual verification

  • npm run typecheck (admin) — exit 0
  • npx tsc --noEmit (admin) — exit 0
  • ./scripts/verify.sh — exit 0
  • Diff is limited to the dependency list and the cancelled / getSnapRef plumbing. The data-loading code path is otherwise unchanged: same endpoints, same payload mapping.

Files touched

project/apps/admin/src/features/products/components/ProductEditor.tsx (modified)