From 31b54c4497b22cf1537c92266d486de8e25ce509 Mon Sep 17 00:00:00 2001 From: aloktomarr Date: Sat, 25 Jul 2026 21:05:04 +0000 Subject: [PATCH] fix(DataViewFilters): avoid JSON.stringify crash on React element filter labels childrenHash serialized each child's full props via JSON.stringify. When a DataViewCheckboxFilter option label is a React element (e.g. an icon), the props contain React elements whose owner-fiber references are circular, so JSON.stringify threw 'cyclic object value' and the component crashed on render. Add a replacer that serializes any nested React element as a stable { type, key } descriptor so hashing no longer recurses into circular internals. childrenHash only gates the filterItems memo (which reads filterId/title), so behavior is unchanged. Includes a regression test. Fixes patternfly/patternfly-react#12536 --- .../DataViewFilters/DataViewFilters.test.tsx | 22 +++++++++++++++++++ .../src/DataViewFilters/DataViewFilters.tsx | 18 ++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/module/src/DataViewFilters/DataViewFilters.test.tsx b/packages/module/src/DataViewFilters/DataViewFilters.test.tsx index 172ab6f6..498c87aa 100644 --- a/packages/module/src/DataViewFilters/DataViewFilters.test.tsx +++ b/packages/module/src/DataViewFilters/DataViewFilters.test.tsx @@ -2,6 +2,7 @@ import { render, fireEvent } from '@testing-library/react'; import DataViewFilters from './DataViewFilters'; import DataViewToolbar from '../DataViewToolbar'; import DataViewTextFilter from '../DataViewTextFilter'; +import { DataViewCheckboxFilter } from '../DataViewCheckboxFilter'; describe('DataViewFilters component', () => { const mockOnChange = jest.fn(); @@ -37,4 +38,25 @@ describe('DataViewFilters component', () => { fireEvent.input(input, { target: { value: 'abc' } }); expect(mockOnChange).toHaveBeenCalledWith('one', { one: 'abc' }); }); + + it('should not crash when a checkbox filter option label is a React element', () => { + // Regression test for #12536: using a React element (e.g. an icon) as a + // DataViewCheckboxFilter option label made childrenHash's JSON.stringify throw on the + // element's circular references. The tree is created inside a wrapper component so the + // label element has an owner (as in real usage), which is what triggers the cycle. + const FilterWithElementLabel = () => ( + + Active, value: 'active' } ]} + /> + + } + /> + ); + expect(() => render()).not.toThrow(); + }); }); diff --git a/packages/module/src/DataViewFilters/DataViewFilters.tsx b/packages/module/src/DataViewFilters/DataViewFilters.tsx index dc0dc218..07e78209 100644 --- a/packages/module/src/DataViewFilters/DataViewFilters.tsx +++ b/packages/module/src/DataViewFilters/DataViewFilters.tsx @@ -33,6 +33,21 @@ export interface DataViewFiltersProps extends Omit { + if (isValidElement(value)) { + const { type } = value; + const typeName = + typeof type === 'string' + ? type + : (type as { displayName?: string; name?: string }).displayName ?? (type as { displayName?: string; name?: string }).name ?? 'element'; + return { type: typeName, key: value.key }; + } + return value; +}; export const DataViewFilters = ({ children, @@ -52,7 +67,8 @@ export const DataViewFilters = ({ const childrenHash = useMemo(() => JSON.stringify( Children.map(children, (child) => isValidElement(child) ? { type: child.type, key: child.key, props: child.props } : child - ) + ), + replaceReactElements ), [ children ]); const filterItems: DataViewFilterIdentifiers[] = useMemo(() => Children.toArray(children)