Naresh
Naresh

Reputation: 25803

How to add properties to child components using React.cloneElement?

[Note: CodeSandbox is here]

I am trying to implement a ToggleButtonGroup which "selects" a button when clicked. Only one button can be selected at a time.

enter image description here

The usage should be as follows, where clicking on any of the buttons, sends an onChange event to the parent with the button's value:

export default function App() {
  const [color, setColor] = useState<string | undefined>();

  return (
    <div className="App">
      <ToggleButtonGroup value={color} onChange={setColor}>
        <ToggleButton value="R">Red</ToggleButton>
        <ToggleButton value="G">Green</ToggleButton>
        <ToggleButton value="B">Blue</ToggleButton>
      </ToggleButtonGroup>

      <h4>Color: {color}</h4>
    </div>
  );
}

I understand that the standard way to do this is to add properties to the child components (in this case ToggleButton) using React.cloneElement(). Here's my attempt to do this, but it is not working:

interface ToggleButtonProps {
  value: string;
  selected?: boolean;
  children: ReactNode;
}

const ToggleButton = ({ selected, children }: ToggleButtonProps) => {
  return <button className={selected ? "selected" : ""}>{children}</button>;
};

interface ToggleButtonGroupProps {
  value: string | undefined;
  children: ReactNode;
  onChange: (value: string) => void;
}

const ToggleButtonGroup = ({
  value,
  children,
  onChange
}: ToggleButtonGroupProps) => {
  const buttons = React.Children.map(children, (child) => {
    React.cloneElement(child, {
      selected: child.props.value === value,
      onClick: () => {
        onChange(child.props.value);
      }
    });
  });

  return <div>{buttons}</div>;
};

The cloned ToggleButtons are not even rendering!

  1. What am I doing wrong? How do I make this work?
  2. There are a couple of TypeScript errors in ToggleButtonGroup. How do I fix them?

My CodeSandbox is here.

Upvotes: 0

Views: 644

Answers (1)

Oleksandr Kovalenko
Oleksandr Kovalenko

Reputation: 616

Solution

Add onClick to ToggleButton component:

// ---------- ToggleButton ----------
interface ToggleButtonProps {
  value: string;
  selected?: boolean;
  children: ReactNode;
  onClick: () => void;
}

const ToggleButton = ({ selected, children, onClick }: ToggleButtonProps) => {
  return <button onClick={onClick} className={selected ? "selected" : ""}>{children}</button>;
};

Alternative solution

However, I usually find that it's never worth it to mess with JSDom nodes.

For your particular case I would suggest passing props for your ButtonGroup directly and let ButtonGroup render the list.

Using Omit so I won't have to pass selected and onClick as they will be set by ButtonGroup.

interface ToggleButtonGroupProps {
  value: string;
  options: Omit<ToggleButtonProps, "selected" | "onClick">[];
  onChange: (newValue: string) => void;
}

const ToggleButtonGroup: React.FC<ToggleButtonGroupProps> = (props) => (
  <div className="toggle-button-group">
    {props.options.map((option) => (
      <ToggleButton
        key={option.label}
        value={option.value}
        onClick={() => props.onChange(option.value)}
        label={option.label}
        selected={option.value === props.value}
      />
    ))}
  </div>
);

Full example on codesandbox.io

Upvotes: 1

Related Questions