Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/column-layout/flexible-column-layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import clsx from 'clsx';

import { useContainerQuery } from '@cloudscape-design/component-toolkit';

import { flattenChildren } from '../../internal/utils/flatten-children';
import { InternalColumnLayoutProps } from '../interfaces';

import styles from './styles.css.js';
Expand Down
2 changes: 1 addition & 1 deletion src/column-layout/grid-column-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import clsx from 'clsx';

import { GridProps } from '../grid/interfaces';
import InternalGrid from '../grid/internal';
import { useContainerBreakpoints } from '../internal/hooks/container-queries';
import { flattenChildren } from '../internal/utils/flatten-children';
import { InternalColumnLayoutProps } from './interfaces';
import { COLUMN_TRIGGERS, ColumnLayoutBreakpoint } from './internal';
import { repeat } from './util';
Expand Down
2 changes: 1 addition & 1 deletion src/grid/internal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import clsx, { ClassValue } from 'clsx';

import { useMergeRefs, warnOnce } from '@cloudscape-design/component-toolkit/internal';
Expand All @@ -11,6 +10,7 @@ import { Breakpoint, matchBreakpointMapping } from '../internal/breakpoints';
import { useContainerBreakpoints } from '../internal/hooks/container-queries';
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
import { isDevelopment } from '../internal/is-development';
import { flattenChildren } from '../internal/utils/flatten-children';
import { GridProps } from './interfaces';

import styles from './styles.css.js';
Expand Down
70 changes: 70 additions & 0 deletions src/internal/utils/__tests__/flatten-children.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { Fragment } from 'react';

import { flattenChildren } from '../flatten-children';

describe('flattenChildren', () => {
const nestedArrayChildren = [
<div key="1">Item 1</div>,
[<div key="2">Item 2</div>, <div key="3">Item 3</div>],
<div key="4">Item 4</div>,
];

const fragmentChildren = [
<Fragment key="group">
<span key="a">A</span>
<span key="b">B</span>
</Fragment>,
<span key="c">C</span>,
];

const singleFragment = (
<Fragment>
<span>A</span>
<span>B</span>
</Fragment>
);

// Tests React 19+ behavior: uses Children.toArray() which does NOT flatten fragments
describe('React 19+', () => {
beforeEach(() => {
// Mock React.version to trigger Children.toArray() code path
Object.defineProperty(React, 'version', {
value: '19.0.0',
writable: true,
configurable: true,
});
});

it('flattens nested arrays', () => {
expect(flattenChildren(nestedArrayChildren)).toHaveLength(4);
});

it('does NOT flatten fragments', () => {
expect(flattenChildren(fragmentChildren)).toHaveLength(2);
expect(flattenChildren(singleFragment)).toHaveLength(1);
});
});

// Tests React 16-18 behavior: uses react-keyed-flatten-children which DOES flatten fragments
describe('React 16-18', () => {
beforeEach(() => {
// Mock React.version to trigger react-keyed-flatten-children code path
Object.defineProperty(React, 'version', {
value: '18.2.0',
writable: true,
configurable: true,
});
});

it('flattens nested arrays', () => {
expect(flattenChildren(nestedArrayChildren)).toHaveLength(4);
});

it('flattens fragments', () => {
expect(flattenChildren(fragmentChildren)).toHaveLength(3);
expect(flattenChildren(singleFragment)).toHaveLength(2);
});
});
});
17 changes: 17 additions & 0 deletions src/internal/utils/flatten-children.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { ReactNode } from 'react';
import flattenChildrenLegacy from 'react-keyed-flatten-children';

export function flattenChildren(children: ReactNode): ReactNode[] {
const majorVersion = parseInt(React.version.split('.')[0], 10);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since React.version is undocumented, should we make this line more failproof? And if checking the version fails, then fall back to the new behavior (i.e, not handling fragments, same as version >= 19)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I will do it like that.


if (majorVersion >= 19) {
// React 19+: Uses Children.toArray() which doesn't flatten fragments.
// This also supports bigint which is not available in earlier React versions.
return React.Children.toArray(children);
} else {
// React 16-18: Use react-keyed-flatten-children for backward compatibility
return flattenChildrenLegacy(children);
}
}
7 changes: 4 additions & 3 deletions src/space-between/internal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { forwardRef } from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import clsx from 'clsx';

import { useMergeRefs } from '@cloudscape-design/component-toolkit/internal';

import { getBaseProps } from '../internal/base-component';
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
import { flattenChildren } from '../internal/utils/flatten-children';
import WithNativeAttributes from '../internal/utils/with-native-attributes';
import { SpaceBetweenProps } from './interfaces';

Expand Down Expand Up @@ -52,10 +52,11 @@ const InternalSpaceBetween = forwardRef(
ref={mergedRef}
>
{flattenedChildren.map(child => {
const key = typeof child === 'object' ? child.key : undefined;
// If this react child is a primitive value, the key will be undefined
const key = (child as Record<'key', unknown>).key;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a possible case where this will throw an error? Let's say some conditional rendering logic where a child can be rendered as undefined or a number.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the child is a number, bigint (in react 19), or string the key will be undefined. so we will be assigning undefined key. But it shouldn't throw an error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what about undefined?

Before the changes, the code was safe because typeof undefined is not 'object', therefore we render undefined according to the ternary operator, but after the changes, we will try to access a property of undefined, which throws an error.

Copy link
Member Author

@amanabiy amanabiy Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that accessing a property on undefined would throw an error. However, this is safe because both Children.toArray() and react-keyed-flatten-children filter out undefined, null, and boolean values.

Looking at the type signature of toArray it returns the following: toArray(children: ReactNode | ReactNode[]): Array<Exclude<ReactNode, boolean | null | undefined>>; The return type explicitly excludes undefined, null, and boolean. React's Children.toArray() implementation (test example). Under the hood, react-keyed-flatten-children also uses Children.toArray so they are filtered automatically, in both cases.

Here is another place where we also use similar code (line 85). That said, if you'd prefer more safe approach, we could add a guard and/or I can change the other implementation as well. What do you think?


return (
<div key={key} className={styles.child}>
<div key={key ? String(key) : undefined} className={styles.child}>
{child}
</div>
);
Expand Down
Loading