简体   繁体   中英

How do I prevent an entire list from re-rendering when state of one component changes?

I have a list component that fetches data and maps the results over card-like child components to create a list of "cards". Below is a example:

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

  setActiveTab = (activeTab) => {
    this.setState({ activeTab });
  }

  render() {
    const activeTab = this.state.activeTab;

    return (
      <div className="card">
        <ul className="nav nav-tabs card-header-tabs">
          <li 
            className="nav-item" 
            key={ 1 }
            onClick={ () => this.setActiveTab(1) }
          >
            <a className="nav-link" href="#">Overview</a>
          </li>
          <li 
            className="nav-item" 
            key={ 2 }
            onClick={ () => this.setActiveTab(2) }
          >
            <a className="nav-link" href="#">Data</a>
          </li>
          <li 
            className="nav-item" 
            key={ 3 }
            onClick={ () => this.setActiveTab(3) }
          >
            <a className="nav-link" href="#">Edit</a>
          </li>
        </ul>
        <h3 className="card-title">{ this.props.title }</h3>
        { activeTab === 1 && 
          <h5 className="card-text">
            { this.props.body }
          </h5>    
        }
        { activeTab === 2 && 
          <div className="card-data">
            <a>Yes: { this.props.yes }</a>
            <a>No: { this.props.no }</a>
          </div>
        }
        { activeTab === 3 &&
          <div>
            <a 
              href="/cards"
              onClick={ this.props.onDelete }
              className="delete-card"
            >
              Delete
            </a>
          </div>
        }
      </div>
    )
  }
}
export default Card;


class CardList extends Component {
  componentDidMount() {
    this.props.fetchCards();
  }

  renderCards() {
    return this.props.cards.reverse().map(({ _id, title, body, yes, no }) => {
      return (
        <Card
          key={_id}
          title={title}
          body={body}
          yes={yes}
          no={no}
          onDelete={() => this.props.deleteSurvey(_id)}
        />
      );
    });
  }

  render() {
    return (
      <div className="container">
        <div className="row">{this.renderCards()}</div>
      </div>
    );
  }
}

Each card has tabs, clicking a tab determine which text is shown in each particular card. The tab/toggle is working, but my issue is that the entire list is re-rendering when the tab is clicked. So if you are at the bottom of the list, the re-render brings you back to the top of the list.

I've tried implementing componentShouldUpdate, but with no success thus far. I would like for the card to toggle it's content and the list remain in place. Is using the shouldUpdate lifecycle method the correct route? Or is there a better approach?

I think it's the prbolem

 onClick={ () => this.setActiveTab(2) }

Essentially, React decides to re-render a component when it detects a change in component state or props, and it only does shallow comparison for object.

As you are using closures, these function is dynamically re-created every time the Card component render function is called. React detects a new object (in JS, function is treated as object) passed to the li element, then it redraws it.

To solve this problem you can do something like:

<li className="nav-item" data-tab={ 1 }
                            onClick={this.navLinkClickHandler}>
                            <a className="nav-link" href="#">Overview</a>
                        </li>

Then have navLinkClickHandler defined in your component:

navLinkClickHandler = (evt) => this.setState({activeTab: evt.target.dataset.tab})

More on React reconcilation can be found here:

https://reactjs.org/docs/reconciliation.html

And about using closure in render function:

https://reda-bx.gitbooks.io/react-bits/content/conventions/08.closures-in-render.html

In your <a> anchor tag you have href="#" ` which will always re-direct you to a new link. (# will re-direct to current page re-rendering everything).

Easiest Solution is to remove href="#" which will remove your cursor: pointer; styling but you can re-add that back into your nav-item classname.

Another simple solution is you can move the onClick into the anchor tag and adding evt.preventDefault() (Which prevents the event handler from doing the default action which is loading the page to the href) However this will make it so that you have to click the anchor tag so if you have padding between the <li> and <a> this might not be your best solution.

<li 
  className="nav-item"
  key={ 3 } 
>
  <a 
    className="nav-link" 
    href="#" 
    onClick={ (e) => { e.preventDefault(); this.setActiveTab(3); } }
  >
    Edit
  </a>
</li>

However Thai Duong Tran made a good point where you don't want to re-create the function every time.

Best solution is to move your anonymous function into a class function. (This solution also removes the href="#" so you need to add cursor: pointer; into your css if you want that style)

 <li 
   className="nav-item" 
   data-tab={ 1 }
   onClick={this.onNavClick}
 >
   <a className="nav-link">Overview</a>
 </li>

 onNavClick = (evt) => {
   this.setState({activeTab: evt.target.dataset.tab});
 }

Also your delete action has the same issue where you added a href="/card" so you want to be careful with that.

If you want to have route actions to move to different pages/URL. You should look into adding react-router

Try this code inside CARD component (If you have not tried it already). Might work fine.

shouldComponentUpdate(nextProps,nextState){
   if(this.state.activeTab  === nextState.activeTab ){
      return false
   } else {
      return true
   }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM