Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Need React.Children.map() and React.cloneElement() examples #12267

Closed
tkrotoff opened this issue Oct 26, 2016 · 8 comments
Closed

Need React.Children.map() and React.cloneElement() examples #12267

tkrotoff opened this issue Oct 26, 2016 · 8 comments

Comments

@tkrotoff
Copy link
Contributor

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

@vvakame
Copy link
Member

vvakame commented Oct 28, 2016

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

@tkrotoff
Copy link
Contributor Author

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

@vvakame
Copy link
Member

vvakame commented Nov 1, 2016

me too. please make conversations with definition authors.

@vsiao
Copy link
Contributor

vsiao commented Nov 10, 2016

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
);

@tkrotoff
Copy link
Contributor Author

@vsiao thx a lot, it works!

@tkrotoff
Copy link
Contributor Author

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
  })
);

@variousauthors
Copy link

variousauthors commented Jan 15, 2019

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants