React.cloneElement not appending className

How can I append or change the className prop with React.cloneElement?

When I use React.cloneElement I’m unable to change or append the className prop. I’ve searched for hours but I found nothing. React.Children.only or removing the spread don’t change the behavior. It appear to be a bug, or a performance optimization feature?.

Expect html: <div class="parent"><div class="child other-class">testing...</div></div>

Result html: <div class="parent"><div class="child">testing...</div></div>

Class example:

class Parent extends React.Component {
  render() {
    return (
      <div className={"parent"}>
        {React.cloneElement(React.Children.only(this.props.children), {
          ...this.props.children.props,
          className: `${this.props.children.props.className} other-class`,
        })}
      </div>
    );
  }
}

class Child extends React.Component {
  render() {
    return <div className={"child"}>{"testing..."}</div>;
  }
}

Functional component example:

const Parent = ({ children }) => (
  <div className={"parent"}>
    {React.cloneElement(React.Children.only(children), {
      ...children.props,
      className: `${children.props.className} other-class`,
    })}
  </div>
);

const Child = () => <div className={"child"}>{"testing..."}</div>;

const Parent = ({ children }) => (
  <div className={"parent"}>
    {React.cloneElement(React.Children.only(children), {
      ...children.props,
      className: `${children.props.className} other-class`,
    })}
  </div>
);

const Child = () => <div className={"child"}>{"testing2..."}</div>;

ReactDOM.render(
  <React.StrictMode>
    <Parent>
      <Child />
    </Parent>
  </React.StrictMode>,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Answer

The problem is that you’re never using className in Child, which is what you’re manipulating in Parent. Child puts a className on a div, but that isn’t Child‘s className, it’s just a hardcoded one that Child puts on the div.

If you want Child to put that class on the div, you have to write the code to do that. Also, you don’t need the spread, the props are merged. Finally, to get the original className, I’d use the result of calling Children.only, rather than going back to this.props.children (though that will work because only would throw if there weren’t only one).

See comments:

class Parent extends React.Component {
    render() {
        // Get the `className` from the child after verifying there's only one
        const child = React.Children.only(this.props.children);
        const className = `${child.props.className} other-class`;
        return (
            <div className={"parent"}>
                {React.cloneElement(child, {
                    // No need to spread previous props here
                    className,
                })}
            </div>
        );
    }
}

class Child extends React.Component {
    render() {
        // Use `className` from `Child`'s props
        const className = (this.props.className || "") + " child";
        return <div className={className}>{"testing..."}</div>;
    }
}

// Note the `classname` on `Child`, to show that your code using
// `this.props.children.props.className`
ReactDOM.render(<Parent><Child className="original"/></Parent>, document.getElementById("root"));
.child {
    color: red;
}
.child.other-class {
    color: green;
}
.original {
    font-style: italic;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>