Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: cloneElement does not correctly merge styles when used in nested components #32531

Open
User6531 opened this issue Mar 5, 2025 · 1 comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@User6531
Copy link

User6531 commented Mar 5, 2025

Description:

I encountered an issue when using React.cloneElement in two separate components that wrap an SVG icon. When both components use cloneElement, the styles from the first wrapper are lost in the second wrapper.

Steps to Reproduce:

  1. IconResizer applies width, height, and flexShrink styles.
  2. IconColorizer applies color: #FFC700.
  3. When both are used together, only the styles from IconResizer remain, and the color is not applied.

Code Example:

import React, { cloneElement, ReactElement } from 'react';

const IconResizer = ({ children }: { children: ReactElement }) => {
  return cloneElement(children, {
    style: { height: '16px', width: '16px', flexShrink: 0 },
  });
};

const IconColorizer = ({ children }: { children: ReactElement }) => {
  return cloneElement(children, {
    style: { ...(children.props.style || {}), color: '#FFC700' },
  });
};

const MyIcon = () => (
  <IconResizer>
    <IconColorizer>
      <svg
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M14.0001 24C13.7837 24..."
          fill="currentColor"
        />
      </svg>
    </IconColorizer>
  </IconResizer>
);

export default MyIcon;

Expected Behavior:

The svg element should have both height: 16px, width: 16px, flexShrink: 0 and color: #FFC700 applied.

Actual Behavior:

  • The height and width from IconResizer are applied correctly.
  • The color from IconColorizer is missing.

Environment:

  • React version: 18.3.1
  • Browser: chrome

Possible Cause:

It seems that the second cloneElement is not properly merging the styles from the first cloneElement. This behavior is unexpected, as cloneElement is supposed to preserve existing props while overriding the specified ones.


Additional Notes:

  • If I manually spread the styles when creating the first cloneElement, the issue persists.
  • Using a single cloneElement works fine, but nesting them causes the first one's styles to be lost.

Would appreciate insights on whether this is expected behavior or a bug. Thanks!

@User6531 User6531 added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Mar 5, 2025
@alex-r-bigelow
Copy link

Not a react dev, but Google sent me here for a similar issue.

Took me a while to figure out, but React passes in the merged style to your component as its style prop. IconColorizer needs to use the merged style prop that React is giving it, not try to look for already-merged props on the child:

Per the docs:

Cloning an element does not modify the original element.

Fixed code

(I made a few changes for typescript / multiple children / React 19 reasons, as I was using your example to debug my own; thank you!)

import {
  Children,
  cloneElement,
  CSSProperties,
  isValidElement,
  ReactElement,
} from 'react';

const IconResizer = ({
  children,
}: {
  children: ReactElement<SVGElement> | ReactElement<SVGElement>[];
}) => {
  return (
    <>
      {Children.map(children, (child) => {
        if (!isValidElement(child)) {
          return;
        }
        return cloneElement(child, {
          style: {
            ...(child.props.style || {}),
            height: '16px',
            width: '16px',
            flexShrink: '0',
          } as CSSStyleDeclaration,
        });
      })}
    </>
  );
};

const IconColorizer = ({
  children,
  style,
}: {
  children: ReactElement<SVGElement> | ReactElement<SVGElement>[];
  style?: CSSProperties;
}) => {
  return (
    <>
      {Children.map(children, (child) => {
        if (!isValidElement(child)) {
          return;
        }
        return cloneElement(child, {
          style: {
            ...(style || {}),
            color: '#FFC700',
          } as CSSStyleDeclaration,
        });
      })}
    </>
  );
};

export const MyIcon = () => (
  <IconResizer>
    <IconColorizer>
      <svg
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path d="M0 0 L24 24 L0 24 Z" fill="currentColor" />
      </svg>
    </IconColorizer>
  </IconResizer>
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

2 participants