8

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?

2 Answers 2

6

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.

1
  • Note that returning React.Children.map directly inside render is only possible with newer versions of React. You'd need to wrap it in some other element like a div for older versions of React. May 12, 2018 at 18:05
2

I haven't come to the conclusion if it is anti-pattern or not, but there are pros and cons to each approach. In my actual implementation I support both interfaces to improve usability by downstream developers. The developer can provide an actual React element for full control or props for default behavior.


Using cloneElement

Pros

  • Give the consumer full control of the button instance, except the props you need to access/add/modify.

Cons

  • Have to know exactly what to overwrite (will become unmaintainable on nested elements). So it essentially is only good for superficial modification.
  • Harder to extend / modify the element downstream (previous point).

Passing props

Pros

  • Much easier to extend the element via JSX.
  • Extension follows traditional pattern of parent / child in React.

Cons

  • Less control to the consumer. They may have specific button components on their page, and all of a sudden they can only pass in props / label to control this component.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.