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?

James
  • 209
  • 1
  • 2
  • 6

2 Answers2

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.

Josh Broadhurst
  • 179
  • 1
  • 5
  • 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. – Josh Broadhurst May 12 '18 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.
James
  • 209
  • 1
  • 2
  • 6