Definitelytyped: Need React.Children.map() and React.cloneElement() examples

Created on 26 Oct 2016  ·  8Comments  ·  Source: DefinitelyTyped/DefinitelyTyped

React.Children.map() is missing a good example, I don't understand how to type it:

/*
<CheckboxGroup ...>
  <Checkbox ... />
  <Checkbox ... />
  <Checkbox ... />
</CheckboxGroup>
*/

// Child
class Checkbox extends React.Component<CheckboxProps, void> {
  // ...
}

// Parent
class CheckboxGroup extends React.Component<CheckboxGroupProps, void> {
  // ...

  render() {
    const checkboxes = React.Children.map(this.props.children, checkbox =>
      React.cloneElement(checkbox, {
        name: this.props.name,
        checked: this.props.checkedValues.includes(checkbox.props.value),
        onChange: this.handleCheckboxChange.bind(this)
      })
    );

    return (
      <div>
        {checkboxes}
      </div>
    );
  }
}

(very common example, see http://stackoverflow.com/a/32371612/990356)

  • How to type this.props.children?
  • How to type React.cloneElement()?
  • How to type React.Children.map()?

Most of the time I end up with the following error: Type 'string' is not assignable to type 'ReactElement<any>', see #8131

Most helpful comment

Question:

const checkboxes = React.Children.map(this.props.children, checkbox =>
  React.cloneElement(checkbox, {
    name: this.props.name,
    checked: this.props.checkedValues.includes(checkbox.props.value),
    onChange: this.handleCheckboxChange.bind(this)
  })
);

Solution:

const checkboxes = React.Children.map(props.children, (checkbox: React.ReactElement<CheckboxPropsInternal>) =>
  React.cloneElement(checkbox, {
    checked: props.checkedValues.includes(checkbox.props.value),
    onChange: handleCheckboxChange
  })
);

All 8 comments

please send a pull request. I'll review it.

@vvakame I can't, I don't understand how it is supposed to work

me too. please make conversations with definition authors.

you'll need to type-annotate or type-assert checkbox explicitly because there's no guarantee that this.props.children does not contain non-ReactElement values. for example:

React.Children.map(this.props.children, (checkbox: React.ReactElement<CheckboxProps>) => 
    React.cloneElement(checkbox) // should be ok
);

@vsiao thx a lot, it works!

Question:

const checkboxes = React.Children.map(this.props.children, checkbox =>
  React.cloneElement(checkbox, {
    name: this.props.name,
    checked: this.props.checkedValues.includes(checkbox.props.value),
    onChange: this.handleCheckboxChange.bind(this)
  })
);

Solution:

const checkboxes = React.Children.map(props.children, (checkbox: React.ReactElement<CheckboxPropsInternal>) =>
  React.cloneElement(checkbox, {
    checked: props.checkedValues.includes(checkbox.props.value),
    onChange: handleCheckboxChange
  })
);

@tkrotoff you could also do this:

    React.Children.map(this.props.children, (child: number) => {
      return child + 1
    })

This and the suggested solution are just taking advantage of the fact that React.Children.map is not strongly typed. You might as well have used any instead of the Checkbox type since there is no guarantee that that will be the type of the children. This means that you are likely to have runtime errors if anyone miss-uses the component by passing in a string.

How about this:

class Slot extends React.PureComponent {
  render () {
    return this.props.children
  }
}

const isReactElement = (obj: {}): obj is React.ReactElement<{}> => {
  return obj.hasOwnProperty('type')
}

const isSlot = (obj: {}): obj is Slot => {
  return isReactElement(obj) && obj.type === Slot
}

const getSlots = (children: React.ReactNode) => React.Children.map(children, (child: React.ReactChild): JSX.Element | null => {
  if (isReactElement(child) && isSlot(child)) {
    return child
  }

  return null
})

class ComponentWithSlots extends React.PureComponent {
  render () {
    const [header, footer] = getSlots(this.props.children)

    return (
      <div>
        {header}
        <div>
          <h1>Welcome</h1>
          <p>This is my lovely component with slots</p>
        </div>
        {footer}
      </div>
    )
  }
}

class MyComponent extends React.PureComponent {
  render () {
    return (
      <ComponentWithSlots>
        <Slot>My Header!</Slot>
        <Slot>
          <div>github: @variousauthors</div>
        </Slot>
      </ComponentWithSlots>
    )
  }
}

Which will render:

My Header!
Welcome
This is my lovely component with slots

github: @variousauthors

This approach takes advantage of the type information we _do_ have, which is that a ReactChild might be a ReactElement (I've used type to detect this, but you could be more careful if you wanted). We can also be more descriptive and elaborate that Slot. I saw a UI library implement a pattern where classes had their own slots attached. Something like:

<Dropdown>
  <Dropdown.Header>
    <FancyIcon>My Header!</FancyIcon>
  </Dropdown.Header>
  {dropdownItems}
</Dropdown>

This way the user is clear on the meaning of each slot. The rest of the children (those not parsed out as slots) are left in a collection called children and rendered a la cart, allowing the user to still use children normally while reserving special behaviour for the custom slots.

EDIT: React apparently has a method React.isValidElement which you can use in place of my custom isReactElement.

EDIT: Something else you can do is type the children property on your component, so that a user is only allowed to pass in Checkboxes as children. This will help avoid those runtime errors.

Was this page helpful?
0 / 5 - 0 ratings