diff --git a/src/internal/components/dropdown/context.tsx b/src/internal/components/dropdown/context.tsx
index 610cc48396..4452740382 100644
--- a/src/internal/components/dropdown/context.tsx
+++ b/src/internal/components/dropdown/context.tsx
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import React, { useContext } from 'react';
+import React, { useContext, useMemo } from 'react';
type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
@@ -14,7 +14,8 @@ export interface DropdownContextProviderProps {
}
export function DropdownContextProvider({ children, position = 'bottom-right' }: DropdownContextProviderProps) {
- return {children};
+ const value = useMemo(() => ({ position }), [position]);
+ return {children};
}
export function useDropdownContext() {
diff --git a/src/table/use-column-widths.tsx b/src/table/use-column-widths.tsx
index db41a79c8c..6597a10922 100644
--- a/src/table/use-column-widths.tsx
+++ b/src/table/use-column-widths.tsx
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
+import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useResizeObserver, useStableCallback } from '@cloudscape-design/component-toolkit/internal';
import { getLogicalBoundingClientRect } from '@cloudscape-design/component-toolkit/internal';
@@ -35,12 +35,12 @@ function readWidths(
}
function updateWidths(
- visibleColumns: readonly ColumnWidthDefinition[],
+ columnById: Map,
oldWidths: Map,
newWidth: number,
columnId: PropertyKey
) {
- const column = visibleColumns.find(column => column.id === columnId);
+ const column = columnById.get(columnId);
let minWidth = DEFAULT_COLUMN_WIDTH;
if (typeof column?.width === 'number' && column.width < DEFAULT_COLUMN_WIDTH) {
minWidth = column?.width;
@@ -83,61 +83,84 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain
const containerWidthRef = useRef(0);
const [columnWidths, setColumnWidths] = useState>(null);
+ // Pre-build a Map for column lookups
+ const columnById = useMemo(
+ () => new Map(visibleColumns.map(column => [column.id, column])),
+ [visibleColumns]
+ );
+
const cellsRef = useRef(new Map());
const stickyCellsRef = useRef(new Map());
+ // Cache measured widths from real cells to avoid getBoundingClientRect() during render
+ const measuredWidthsRef = useRef(new Map());
const getCell = (columnId: PropertyKey): null | HTMLElement => cellsRef.current.get(columnId) ?? null;
- const setCell = (sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => {
+ const setCell = useCallback((sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => {
const ref = sticky ? stickyCellsRef : cellsRef;
if (node) {
ref.current.set(columnId, node);
} else {
ref.current.delete(columnId);
}
- };
+ }, []);
- const getColumnStyles = (sticky: boolean, columnId: PropertyKey): ColumnWidthStyle => {
- const column = visibleColumns.find(column => column.id === columnId);
- if (!column) {
- return {};
- }
+ const getColumnStyles = useCallback(
+ (sticky: boolean, columnId: PropertyKey): ColumnWidthStyle => {
+ const column = columnById.get(columnId);
+ if (!column) {
+ return {};
+ }
- if (sticky) {
- return {
- width:
- cellsRef.current.get(column.id)?.getBoundingClientRect().width ||
- (columnWidths?.get(column.id) ?? column.width),
- };
- }
+ if (sticky) {
+ // Use cached measured width to avoid getBoundingClientRect() during render.
+ // The cache is populated by updateColumnWidths() which runs outside the render path.
+ return {
+ width: measuredWidthsRef.current.get(column.id) || columnWidths?.get(column.id) || column.width,
+ };
+ }
- if (resizableColumns && columnWidths) {
- const isLastColumn = column.id === visibleColumns[visibleColumns.length - 1]?.id;
- const totalWidth = visibleColumns.reduce(
- (sum, { id }) => sum + (columnWidths.get(id) || DEFAULT_COLUMN_WIDTH),
- 0
- );
- if (isLastColumn && containerWidthRef.current > totalWidth) {
- return { width: 'auto', minWidth: column?.minWidth };
- } else {
- return { width: columnWidths.get(column.id), minWidth: column?.minWidth };
+ if (resizableColumns && columnWidths) {
+ const isLastColumn = column.id === visibleColumns[visibleColumns.length - 1]?.id;
+ const totalWidth = visibleColumns.reduce(
+ (sum, { id }) => sum + (columnWidths.get(id) || DEFAULT_COLUMN_WIDTH),
+ 0
+ );
+ if (isLastColumn && containerWidthRef.current > totalWidth) {
+ return { width: 'auto', minWidth: column?.minWidth };
+ } else {
+ return { width: columnWidths.get(column.id), minWidth: column?.minWidth };
+ }
}
- }
- return {
- width: column.width,
- minWidth: column.minWidth,
- maxWidth: !resizableColumns ? column.maxWidth : undefined,
- };
- };
+ return {
+ width: column.width,
+ minWidth: column.minWidth,
+ maxWidth: !resizableColumns ? column.maxWidth : undefined,
+ };
+ },
+ [columnById, columnWidths, resizableColumns, visibleColumns]
+ );
// Imperatively sets width style for a cell avoiding React state.
// This allows setting the style as soon container's size change is observed.
const updateColumnWidths = useStableCallback(() => {
+ // First pass: set widths on real cells
for (const { id } of visibleColumns) {
const element = cellsRef.current.get(id);
if (element) {
setElementWidths(element, getColumnStyles(false, id));
}
}
- // Sticky column widths must be synchronized once all real column widths are assigned.
+ // Second pass: measure real cell widths and cache them for sticky columns.
+ // This avoids calling getBoundingClientRect() during render.
+ for (const { id } of visibleColumns) {
+ const element = cellsRef.current.get(id);
+ if (element) {
+ const width = element.getBoundingClientRect().width;
+ if (width > 0) {
+ measuredWidthsRef.current.set(id, width);
+ }
+ }
+ }
+ // Third pass: set sticky column widths using cached measurements
for (const { id } of visibleColumns) {
const element = stickyCellsRef.current.get(id);
if (element) {
@@ -189,12 +212,25 @@ export function ColumnWidthsProvider({ visibleColumns, resizableColumns, contain
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- function updateColumn(columnId: PropertyKey, newWidth: number) {
- setColumnWidths(columnWidths => updateWidths(visibleColumns, columnWidths ?? new Map(), newWidth, columnId));
- }
+ const updateColumn = useCallback(
+ (columnId: PropertyKey, newWidth: number) => {
+ setColumnWidths(columnWidths => updateWidths(columnById, columnWidths ?? new Map(), newWidth, columnId));
+ },
+ [columnById]
+ );
+
+ const contextValue = useMemo(
+ () => ({
+ getColumnStyles,
+ columnWidths: columnWidths ?? new Map(),
+ updateColumn,
+ setCell,
+ }),
+ [getColumnStyles, columnWidths, updateColumn, setCell]
+ );
return (
-
+
{children}
);
diff --git a/src/top-navigation/parts/overflow-menu/router.tsx b/src/top-navigation/parts/overflow-menu/router.tsx
index 10c41fb8d7..0e1db40a9a 100644
--- a/src/top-navigation/parts/overflow-menu/router.tsx
+++ b/src/top-navigation/parts/overflow-menu/router.tsx
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import React, { createContext, Dispatch, SetStateAction, useContext, useState } from 'react';
+import React, { createContext, Dispatch, SetStateAction, useContext, useMemo, useState } from 'react';
type View = 'utilities' | 'dropdown-menu';
@@ -60,7 +60,8 @@ interface RouterProps {
const Router = ({ children }: RouterProps) => {
const [state, setState] = useState({ view: 'utilities', data: null });
- return {children};
+ const value = useMemo(() => ({ state, setState }), [state]);
+ return {children};
};
export default Router;