From 74608b7ead4bb62033e833d64911f9605c5f4af6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 00:24:25 +0000 Subject: [PATCH 1/2] fix: Prevent React infinite loop in Tabs component on list page Extract tab contents into memoized components and use useMemo for the tabItems array to prevent the Ant Design Tabs useIndicator hook from causing infinite re-renders when queue state changes. The root cause was that tabItems array was recreated on every render, causing the Tabs component to continuously recalculate its indicator position. By memoizing the array and extracting children into stable React.memo components, each tab's content manages its own queue context subscription independently. --- .../[set_ids]/[angle]/list/layout-client.tsx | 165 ++++++++++-------- 1 file changed, 97 insertions(+), 68 deletions(-) diff --git a/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.tsx b/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.tsx index d1cc5b2e..3714949c 100644 --- a/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.tsx +++ b/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/list/layout-client.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useMemo, useCallback } from 'react'; import { PropsWithChildren } from 'react'; import { Layout, Tabs, Badge, Button, Popconfirm, Flex } from 'antd'; import { DeleteOutlined } from '@ant-design/icons'; @@ -21,80 +21,109 @@ interface ListLayoutClientProps { boardDetails: BoardDetails; } -const TabsWrapper: React.FC<{ boardDetails: BoardDetails }> = ({ boardDetails }) => { +// Queue tab label - subscribes to queue context for badge count +const QueueTabLabel = React.memo(() => { + const { queue } = useQueueContext(); + return ( + + Queue + + ); +}); +QueueTabLabel.displayName = 'QueueTabLabel'; + +// Queue tab content - handles queue display and clear functionality +const QueueTabContent = React.memo(({ boardDetails }: { boardDetails: BoardDetails }) => { const { queue, setQueue } = useQueueContext(); - const handleClearQueue = () => { + const handleClearQueue = useCallback(() => { + const itemsCleared = queue.length; setQueue([]); track('Queue Cleared', { boardLayout: boardDetails.layout_name || '', - itemsCleared: queue.length, + itemsCleared, }); - }; + }, [setQueue, boardDetails.layout_name, queue.length]); - const tabItems = [ - { - key: 'queue', - label: ( - - Queue - - ), - children: ( -
- {queue.length > 0 && ( - - - - - - )} -
- -
-
- ), - }, - { - key: 'search', - label: 'Search', - children: ( -
-
- -
- -
- ), - }, - { - key: 'holds', - label: 'Search by Hold', - children: ( -
-
- -
- -
- ), - }, - ]; + return ( +
+ {queue.length > 0 && ( + + + + + + )} +
+ +
+
+ ); +}); +QueueTabContent.displayName = 'QueueTabContent'; + +// Search tab content +const SearchTabContent = React.memo(({ boardDetails }: { boardDetails: BoardDetails }) => ( +
+
+ +
+ +
+)); +SearchTabContent.displayName = 'SearchTabContent'; + +// Holds search tab content +const HoldsTabContent = React.memo(({ boardDetails }: { boardDetails: BoardDetails }) => ( +
+
+ +
+ +
+)); +HoldsTabContent.displayName = 'HoldsTabContent'; + +// TabsWrapper - no longer subscribes to queue context directly +// Child components handle their own queue subscriptions +const TabsWrapper: React.FC<{ boardDetails: BoardDetails }> = ({ boardDetails }) => { + // Memoize tabItems to prevent infinite re-render loop + // Each tab's children are memoized components that manage their own state + const tabItems = useMemo( + () => [ + { + key: 'queue', + label: , + children: , + }, + { + key: 'search', + label: 'Search', + children: , + }, + { + key: 'holds', + label: 'Search by Hold', + children: , + }, + ], + [boardDetails], + ); return ; }; From 6f20e41a63fd8314065ee67112962cf5109ad5ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 00:25:05 +0000 Subject: [PATCH 2/2] chore: Update package-lock.json --- package-lock.json | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39cd228d..9d1374c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4565,7 +4565,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.57.0" @@ -7263,7 +7263,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -8011,7 +8011,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -10158,7 +10158,7 @@ "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -10647,7 +10647,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.57.0" @@ -10666,7 +10666,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -10898,7 +10898,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10918,7 +10917,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -11285,7 +11283,6 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "dev": true, "license": "MIT" }, "node_modules/scroll-into-view-if-needed": {