3.1 KiB
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
getSnapfrom the load effect's dependency list. The effect now runs once perproductId(a one-shot mount), with an expliciteslint-disable-next-line react-hooks/exhaustive-depsand acancelledflag to drop late responses from a stale request. - Introduced a
getSnapRefthat is updated each render. The load handler now usesgetSnapRef.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
getSnapin 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 0npx tsc --noEmit(admin) — exit 0./scripts/verify.sh— exit 0- Diff is limited to the dependency list and the
cancelled/getSnapRefplumbing. 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)