0
votes

In the below example, Apps (parent) component and its child components loads well on the startup. But if I select the tab (call handleClick) which updating the state with new tab value not re-rendering the child components (tabs, content) with updated value and the child props are not updating with parent state. Please advise. PS: I have added multiple console log for debugging purpose.

import React, { Component } from 'react';
var tabData = [
  {name: 'Tab 1', isActive: true},
  {name: 'Tab 2', isActive: false},
  {name: 'Tab 3', isActive: false}
];
export class Tabs extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeTab: props.activeTab
    };
  }

  componentWillReceiveProps(nextProps) {
    console.log("Tabs:componentWillReceiveProps $$$$$$", this);
    this.setState({activeTab: nextProps.activeTab});
  }

  render() {
    console.log("Tabs $$$$$$", this.props);
    return (

      <ul className="nav nav-tabs">
        {
          tabData.map(function (tab, index) {
            // console.log("####", this.props.activeTab);
            return (
              <Tab key={index} data={tab} isActive={this.props.activeTab === tab}
                   handleClick={this.props.changeTab.bind(this,tab)}/>
            );
          }.bind(this))}
      </ul>
    )
      ;
  }
}
export class Tab extends React.Component {
  componentWillReceiveProps(nextProps) {
    console.log("Tab:componentWillReceiveProps $$$$$$", nextProps);

  }

  render() {
    console.log("Tab $$$$$$", this.props);
    return (
      <li onClick={this.props.handleClick} className={this.props.isActive ? "active" : null}>
        <a href="#">{this.props.data.name}</a>
      </li>
    );
  }
}
export class Content extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeTab: props.activeTab
    };
  }

  componentWillReceiveProps(nextProps) {
    console.log("Tabs:componentWillReceiveProps $$$$$$", nextProps);

  }

  render() {
    console.log("Content $$$$$$", this.state.activeTab);
    console.log("Content props $$$$$$", this.props.activeTab);
    return (
      <div>
        {this.state.activeTab.name === 'Tab 1' ?
          <section className="panel panel-success">
            <h2 className="panel-heading">Content 1</h2>

            <p className="panel-body">chart1</p>

            <p className="panel-body">handsontable1</p>
          </section>
          : null}
        {this.state.activeTab.name === 'Tab 2' ?
          <section className="panel panel-warning">
            <h2 className="panel-heading">Content 2</h2>

            <p className="panel-body">chart2</p>

            <p className="panel-body">handsontable2</p>
          </section>
          : null}
        {this.state.activeTab.name === 'Tab 3' ?
          <section className="panel panel-danger">
            <h2 className="panel-heading">Content 3</h2>

            <p className="panel-body">chart3</p>

            <p className="panel-body">handsontable3</p>
          </section>
          : null}
      </div>
    );
  }
}
export class App extends React.Component {
  constructor(props) {
    super(props);
    //this.activeTabs = tabData[0];
    this.state = {
      activeTab: tabData[0]
    }
  }


  handleClick(tab) {
    debugger;
    console.log("1handleClick*****", tab, this);
    this.setState({activeTab: tab});
    console.log("2handleClick*****", this);
  }

  render() {
    console.log("App $$$$$$", this.state.activeTab);
    return (
      <div>
        <Tabs activeTab={this.state.activeTab} changeTab={this.handleClick}/>
        <Content activeTab={this.state.activeTab}/>
      </div>
    );
  }
}

React.render(
  <App />,
  document.getElementById('app')
);
1

1 Answers

1
votes

You forgot to bind the handleClick event in App component, Use this:

changeTab={this.handleClick.bind(this)}

The way you are using should throw the error, check the console:

Can't read property setState of undefined

Suggestion:

1. It's not a good idea to bind the props functions in child component events, use arrow function to call them.

So instead of:

handleClick={this.props.changeTab.bind(this,tab)}

Use this:

handleClick={() => this.props.changeTab(tab)}

2. Your are using es6, so you can avoid the .bind(this) to maintain the proper context inside map body, use arrow function, like this:

tabData.map( (tab, index) => {
     return (
        <Tab key={index} data={tab} isActive={this.props.activeTab === tab} handleClick={this.props.changeTab.bind(this,tab)}/>
     );
})

Check the working example:

var tabData = [
  {name: 'Tab 1', isActive: true},
  {name: 'Tab 2', isActive: false},
  {name: 'Tab 3', isActive: false}
];

class Tabs extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeTab: props.activeTab
    };
  }

  componentWillReceiveProps(nextProps) {
    this.setState({activeTab: nextProps.activeTab});
  }

  render() {
    return (

      <ul className="nav nav-tabs">
        {
          tabData.map(function (tab, index) {
            return (
              <Tab key={index} data={tab} isActive={this.props.activeTab === tab}
                   handleClick={this.props.changeTab.bind(this,tab)}/>
            );
          }.bind(this))}
      </ul>
    )
      ;
  }
}

class Tab extends React.Component {
  componentWillReceiveProps(nextProps) {
  }

  render() {
    return (
      <li onClick={this.props.handleClick} className={this.props.isActive ? "active" : null}>
        <a href="#">{this.props.data.name}</a>
      </li>
    );
  }
}

class Content extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeTab: props.activeTab
    };
  }

  componentWillReceiveProps(nextProps) {
  }

  render() {
    return (
      <div>
        {this.state.activeTab.name === 'Tab 1' ?
          <section className="panel panel-success">
            <h2 className="panel-heading">Content 1</h2>

            <p className="panel-body">chart1</p>

            <p className="panel-body">handsontable1</p>
          </section>
          : null}
        {this.state.activeTab.name === 'Tab 2' ?
          <section className="panel panel-warning">
            <h2 className="panel-heading">Content 2</h2>

            <p className="panel-body">chart2</p>

            <p className="panel-body">handsontable2</p>
          </section>
          : null}
        {this.state.activeTab.name === 'Tab 3' ?
          <section className="panel panel-danger">
            <h2 className="panel-heading">Content 3</h2>

            <p className="panel-body">chart3</p>

            <p className="panel-body">handsontable3</p>
          </section>
          : null}
      </div>
    );
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeTab: tabData[0]
    }
  }


  handleClick(tab) {
    this.setState({activeTab: tab});
    console.log("2handleClick*****", tab);
  }

  render() {
    return (
      <div>
        <Tabs activeTab={this.state.activeTab} changeTab={this.handleClick.bind(this)}/>
        <Content activeTab={this.state.activeTab}/>
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id='app'/>