From 6bf3366af76c06e36d2d3ec14d138f547dce8d63 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 19 May 2026 17:43:20 +0200 Subject: [PATCH] fix(catalog): stabilise hook order in CatalogPurchaseWidgetView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React reported "Rendered more hooks than during the previous render" when CatalogPurchaseWidgetView transitioned from currentOffer=null to a real offer: hook count jumped from 22 to 23 because the useMemo/useEffect block for the builders-club placement state sat *below* the `if(!currentOffer) return null` early-return on line 140. On the first render it never ran; on the next render (offer loaded) it did, and React's hook-call tracker flagged the divergence and unmounted the component via the error boundary. Fix: move the three builders-club hooks (useMemo builderPlaceableStatus, useMemo buildersClubPlaceOneButtonStyle, useEffect interval) above the early return. They already short-circuit cleanly when isBuildersClubPlaceable is false — added a defensive `!currentOffer` guard on the first useMemo and an explicit `!!currentOffer` clause on the derived isBuildersClubPlaceable so the .product access stays safe when offer is null. Behavior unchanged for the loaded-offer path; the early-render path now runs the hooks but their bodies no-op. Verification: yarn typecheck clean, yarn test 209/209. --- .../views/page/widgets/CatalogPurchaseWidgetView.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx index d5321e0..ed1dc34 100644 --- a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx @@ -137,15 +137,19 @@ export const CatalogPurchaseWidgetView: FC = pro }; }, [ purchaseState ]); - if(!currentOffer) return null; - + // Builders-club state — derived + hooks MUST run unconditionally on + // every render so the hook order stays stable even when currentOffer + // is null (the `if(!currentOffer) return null` below would otherwise + // hide the useMemo/useEffect block from the first render and React + // would flag "Rendered more hooks than during the previous render"). const isBuildersClubOffer = (currentType === CatalogType.BUILDER); const isBuildersClubPlaceable = isBuildersClubOffer + && !!currentOffer && !!currentOffer.product && ((currentOffer.product.productType === ProductTypeEnum.FLOOR) || (currentOffer.product.productType === ProductTypeEnum.WALL)); const builderPlaceableStatus = useMemo(() => { - if(!isBuildersClubPlaceable || !getBuilderFurniPlaceableStatus) return BuilderFurniPlaceableStatus.OKAY; + if(!isBuildersClubPlaceable || !getBuilderFurniPlaceableStatus || !currentOffer) return BuilderFurniPlaceableStatus.OKAY; return getBuilderFurniPlaceableStatus(currentOffer); }, [ currentOffer, getBuilderFurniPlaceableStatus, isBuildersClubPlaceable, builderPlaceableRefreshTick ]); @@ -164,6 +168,8 @@ export const CatalogPurchaseWidgetView: FC = pro return () => clearInterval(interval); }, [ isBuildersClubPlaceable ]); + if(!currentOffer) return null; + const PurchaseButton = () => { if(isBuildersClubPlaceable)