Search code examples
javascriptarraysreactjsthisreact-fullstack

Why does JavaScript's map() work but my for loop fails with React?


I'm teaching myself React and I have come across something I am having trouble understanding. When I build my project with the code in getNavList() below I get the following error when clicking one of the buttons rendered in my component, "cannot read property of undefined 'click' react."

export class Pane extends React.Component {
  constructor(props) {
    super(props);

  }

  getNavList() {
    var buttons = [];
    if(this.props.subNavButtons != null){
      for(var i = 0; i < this.props.navButtons.length; i++) {
        buttons.push(<Button onClick={this.props.navButtons[i].click()} title={this.props.navButtons[i].title} />);
        buttons.push(<Button onClick={this.props.navButtons[i + i].click()} title={this.props.navButtons[i + i].title} />);
        buttons.push(<Button onClick={this.props.navButtons[i + i + 1].click()} title={this.props.navButtons[i + i + 1].title} />);
      }
    } else {
      buttons = this.props.navButtons.map(button => (<Button onClick={() => button.click()} title={button.title}/>));
    }
    return buttons;
  }

  render() {
    return (
      <div>
        <ul>
          {this.getNavList()}
        </ul>
      </div>
    );
  }
}

When I build my project with the following changes, everything works as intended:

export class Pane extends React.Component {
      constructor(props) {
        super(props);

      }

      getNavList() {
        var buttons = [];
        if(this.props.subNavButtons != null){
          buttons = this.props.navButtons.map((button, index) => (<>
                                                                  <Button onClick={() => button.click()} title={button.title}/>
                                                                  <Button onClick={() => this.props.subNavButtons[index + index].click()} title={this.props.subNavButtons[index + index].title}/>
                                                                  <Button onClick={() => this.props.subNavButtons[index + index + 1].click()} title={this.props.subNavButtons[index + index + 1].title}/>
                                                                  </>));
        }
        else {
          buttons = this.props.navButtons.map(button => (<Button onClick={() => button.click()} title={button.title}/>));
        }
        return buttons;
      }

      render() {
        return (
          <div>
            <ul>
              {this.getNavList()}
            </ul>
          </div>
        );
      }
    }

From what I've been able to find, it seems like there's an issue with 'this' losing it's context when I pass the function as a component property. I tried binding the function passed to the Pane object in its parent class to no avail. I also tried defining the function using arrow notation instead with no luck. With that being said, why is it that this change from a for loop to the map function work?

Here's a more minor issue within the same code snippet. With the working getNavList() I return a React.Fragment using the shorthand syntax "< >...< />" but when I try using "< React.Fragment>...< /React.Fragment>" I get an error saying that there is an exppected '}' character at the end of my else statement. What the heck is that about?

Please excuse and spacing errors in my code snippets and React tags. I couldn't figure out how to get them to display without adding additional spaces and I am almost entirely sure that this is not a syntax error but feel free to prove me wrong if that is the case.


Solution

  • onClick={this.props.navButtons[i].click()}

    In your first take at a click event, react is evaluating the property when it's mounting the component. This causes .click() to be called immediately. There are two ways to fix this. One would be to just drop the brackets so that you are not executing your click handler immediately .click. The other option would be to have the click handler return another function (see final example).

    onClick={() => button.click()} title={button.title}

    In your second try, you pass in an anonymous method which calls your .click(). In this case react evaluates the value which is a function and then saves it. React will then call this function when the event is triggered.

    Bonus Points! Sometimes you want to have a value (like an index from a loop) which is used in your click handler. However, when you go to trigger the event it has the wrong value. You can fix this by calling a function which returns a function. It will then allow you to scope that value properly so it's there when you need it.

    myThings.map((x, i) => <thing onClick={this.handleClick(i)} />
    
    handleClick = (index: number) => () => {
       alert(index);
    }
    

    What happens in this example is handleClick is executed on mount seems how it is not wrapped within a function. It takes in the index number and then returns another function which is stored for that click handler. When any of the events get called they have kept a copy of the index value in their scope so you can have access to it.