Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React.js - ForEach as a first-class component?

Tags:

reactjs

I've heard of react-templates, but I was still wondering it was possible to make a first-class ForEach component.

My end goal is to make something like this more readable:

<ul>
  {list.map(function(item, i) {
     return <li>{item}</li>;
   })}
 </ul>

 // instead?
 <ul>
  <ForEach items="{list}">
     <li>{item}</li>
  </ForEach>
 </ul>

Here's my first serious attempt by passing props:

var ForEach = React.createClass({
   render: function(){
      return (
      <ul>
        {this.props.items.map(function(item, i) {
          return React.Children.map(this.props.children, function(child) {
            return React.addons.cloneWithProps(child, {item: item})
         })
        }.bind(this))}
      </ul>
    );
  }
});

var Element = React.createClass({
  render: function(){
    return (
    <li>{this.props.children}</li>
    );
  }
});

// usage within some other React.createClass render:
<ForEach items={['foo', 'bar', 'baz']}>
  <Element>{this.props.item}</Element>
</ForEach>

The challenge I'm running into is what this points to. By single-stepping with a debugger, I can see that I'm creating cloned elements with this.props.item set, but because {this.props.item} is evaluated in the context of some other enclosing component's render method, this isn't the cloned Element component - it's ForEach's parent.

{this.props.item} will work inside Element.render but that's not where I want it - I want to be able to hand Element some expression that interpolates the current item.

Is this just not possible in React, or is there some way that I could have the ForEach component pass down state like current item/index to the nested elements?

UPDATE I can get a significant improvement in readability with ES6 arrow functions. One set of curlies goes away, along with the return (and possibly also a .bind(this) if you reference this inside the loop).

<ul>
  {list.map((item, i) =>
    <li>{item}</li>
  )}
</ul>

That goes a long way to help with the syntax clunkiness of doing map line.

like image 257
wrschneider Avatar asked Nov 12 '15 20:11

wrschneider


1 Answers

My approach would be to have ForEach expect a function child that gets called for each item, and simply injects a react element into the render.

It would be used something like this:

render: function() {
  return (
    <ForEach items={['foo', 'bar', 'baz']}>
      {function (item) {
        return (
          <Element>{item}</Element>
        )
      }/*.bind(this)*/} // optionally use this too
    </ForEach>
  )
}

This would look better yet if you made use of ES6 Arrow Functions:

render() {
  return (
    <ForEach items={['foo', 'bar', 'baz']}>
      {(item) => // bind is not needed with arrow functions
        <Element>{item}</Element>
      } 
    </ForEach>
  )
}

Now, to actually implement ForEach:

var ForEach = React.createClass({
   getDefaultProps: function(){
     return {
       element: 'ul',
       elementProps: {}
     };
   },
   render: function(){
      return React.createElement(
        // Wrapper element tag
        this.props.element,

        // Optional props for wrap element
        this.props.elementProps,

        // Children
        this.props.items.map(this.props.children, this)
      );
   }
});

Pretty simple! The one caveat I've found is that the key prop needs to be set manually by the itterator function (probably using key={index})

Have a look at my basic example

like image 127
MattSturgeon Avatar answered Nov 10 '22 22:11

MattSturgeon