React – Is It Antipattern to Use React.cloneElement to Extend an Element?

anti-patternsinheritancereact

I'm creating a popover component for a UI in React. That component contains a button that triggers the popover to display. Because the button needs to be configurable–label, classes, properties–I need to pass down these configuration parameters to the child. There are two main ways I see this happening.

  1. Pass down label and props as attributes:
<PopoverComponent buttonLabel={label} buttonProps={buttonProps} />
  1. Pass down the actual button.
const button = <Button>Show</Button>;

<PopoverComponent button={button} />

The complication comes inside the popover. In order to place the element I need a ref to the DOM node, so I must add props to the button element inside the PopoverComponent. In React this is straightforward and standard in case 1. Just spread any custom props to the button. E.g., <Button ref="popoverButton" onClick={this.onClick} {...extraProps }>. However, in case 2, we need to React.cloneElement and mutate the props. E.g.,

// INSIDE popoverComponent

const { button } = this.props;

const extendedButton = React.cloneElement(
  button,
  Object.assign(
    {},
    button.props, {
      ref: "popoverButton",
      onClick: this.onClick,
    }),
);

return <div className="popover-control">{ extendedButton }</div>;

Is it antipattern to use React.cloneElement and modify props in child components in React?

Best Answer

Adding additional props to a component is the purpose of React.cloneElement:

Clone and return a new React element using element as the starting point. The resulting element will have the original element’s props with the new props merged in shallowly.

I typically use it in combination with other methods from React.Children API to encapsulate certain functionality in a parent container component. For example, a checked state management container for a sequence of any number of checkboxes:

class CheckboxContainer extends React.Component {
  constructor (props) {
    super();
    this.onClick = this.onClick.bind(this);
    this.state = {
      checked: Array(React.Children.count(props.children)).fill(false)
    };
  }

  onClick (index) {
    this.setState({
      checked: Object.assign([], this.state.checked, {
        index: !this.state.checked[index]
      })
    });
  }

  render () {
    return React.Children.map(this.props.children, (child, index) => {
      return React.cloneElement(child, {
        index: index,
        onClick: () => this.onClick(index),
        checked: this.state.checked[index]
      });
    });
  }
}

class Checkbox extends React.Component {
  render () {
    return (
      <div>
        <input type='checkbox'
          id={`checkbox-${this.props.index}`}
          value={this.props.checked} />
        <label for={`checkbox-${this.props.index}`}>
          {`Child ${this.props.index}`}
        </label>
      </div>
    );
  }
}

class Test extends React.Component {
  render () {
    return (
      <CheckboxContainer>
        <Checkbox />
        <Checkbox />
        <Checkbox />
      </CheckboxContainer>
    );
  }
}

You could do something similar. Have your parent PopoverComponent manage the opened state and use the Button component as a child. Whatever props relate just to the button declare on the button, and merge any props related to managing popover state when you clone it in the parent:

<PopoverComponent>
  <Button label='label' />
</PopoverComponent>

However, I think popovers are special cases because they should appear to render on top of the underlying HTML elements, so it's not a clear parent/child relationship. Also note key and ref are treated differently than other props when using React.cloneElement:

key and ref from the original element will be preserved.

Related Topic