简体   繁体   中英

React: Stop mapped component re-rendering

I have an infinite scroller that calls on a component <Goat /> five at a time giving it random attributes like name and image each time. The scroll towards the bottom and another 5 are loaded in. I have been able to prevent the existing components from re-rendering when the new 5 are displayed with:

shouldComponentUpdate(nextProps){
    return false;
}

This works fine but if I am to pass a unique key to each in the list, it is once again re-rendering all existing <Goat /> components on the page.

Can I prevent this from happening when passing the keys?

App.js:

import React from "react";
import Goat from "./components/addGoat";
import "./App.css";
import Toggle from "./components/toggle";
import InfiniteScroll from "react-infinite-scroll-component";
import uuid from "uuid";
import Header from "./components/Header";
import Theme from "./components/Theme";

class App extends React.Component {
  state = {
    items: Array.from({ length: 5 }),
    darkMode: false
  };

  fetchMoreData = () => {
    this.setState({
      items: this.state.items.concat(Array.from({ length: 5 }))
    });
  };

  getStyle = () => {
    return {
      background: this.state.darkMode ? "#464646" : "#eee",
      transition: "all 0.4s ease",
      color: this.state.darkMode ? "#eee" : "#464646"
    };
  };

  themeToggle = () => {
    console.log("changing style");
    this.setState({
      darkMode: !this.state.darkMode
    });
    this.getStyle();
  };

  render() {
    return (
      <div className="App" style={this.getStyle()}>
        <Theme theme={this.themeToggle} />
        <Header />
        <Toggle>
          <InfiniteScroll
            dataLength={this.state.items.length}
            next={this.fetchMoreData}
            hasMore={true}
            loader={<h4>Loading...</h4>}
            style={this.getStyle()}
          >
            {this.state.items.map((i, index) => (
              <Goat key={uuid.v4()} />
            ))}
          </InfiniteScroll>
        </Toggle>
      </div>
    );
  }
}

export default App;

Goat:

import React, { Component } from "react";

class Goat extends Component {
  state = {
    usedFirstNames: []
  };

  shouldComponentUpdate(nextProps) {
    return false;
  }

  render() {
    const arFirstNames = ["Napoleon", "Albert", "Phil"];

    const arLastNames = ["Jones", "Da Goat", "Williams"];

    const arStoryStart = [
      "Born in hell, this goat is not to be reckoned with.",
      "One of 16 siblings, this goat craves attention.",
      "This French animal loves baguettes... and anything else edible actually."
    ];

    const arStoryMid = [
      "Once tipped off as the next big thing in Winter Sports.",
      "The country people believe that this goat can backflip on command.",
      "Serial eater of buttered baguettes."
    ];

    const arStoryEnd = [
      "This boy has had several medicals and all doctors believe that he will live forever.",
      "Has been warned on numerous occasions that he must stop eating double cheese pizzas before his heart gives out."
    ];

    var rand = Math.floor(Math.random() * 100 + 1);
    var newFirstName = this.state.usedFirstNames.slice();
    let firstName = [];

    do {
      firstName = arFirstNames[Math.floor(Math.random() * arFirstNames.length)];
      this.setState({
        usedFirstNames: [...this.state.usedFirstNames, firstName]
      });
    } while (this.state.usedFirstNames.includes(newFirstName));
    // Fix this buy passing props to app js rather than setting state in this component

    let lastName = arLastNames[Math.floor(Math.random() * arLastNames.length)];
    let randomStory =
      arStoryStart[Math.floor(Math.random() * arStoryStart.length)] +
      " " +
      arStoryMid[Math.floor(Math.random() * arStoryMid.length)] +
      " " +
      arStoryEnd[Math.floor(Math.random() * arStoryEnd.length)];

    //Add check here to see if firstName and/or last name has existing in the previous iterations
    //array.includes array.shift array.push

    let randomName = firstName + " " + lastName;

    return (
      <div className="GoatItem" id="Goats" onScroll={this.handleScroll}>
        <img
          className="GoatImg"
          src={"/images/goats/" + rand + ".jpg"}
          alt="goat"
        />
        <div className="GoatContent">
          <p>{randomName}</p>
          <div className="GoatStory">
            <p>{randomStory}</p>
          </div>
        </div>
      </div>
    );
  }
}

export default Goat;

Current demo without keys - https://lukes-code-infinitescroats.netlify.com

Thanks in advance.

Problem: uuid generates a unique string every time called. Because you're calling it inside render function, which forces all the mapped components to be fed with a new key.

Docs Says:

Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.

So you should not give them a new identity every time anything in the parent component changes.

This Codesandbox shows this exactly. You have to enable paint falshing in your dev tool to check the rerenders.

Solution: Don't use uuid inside render function, instead if you can create an array of objects or if you already have one, create a key which will keep the id for that child. Something like this [{...item, id: uuid() }, ...] .

When you pass the key that is unique, react is not matching them to the original components that are rendered. So even with shouldComponentUpdate: () => false you will still render because they are new components , not components being updated.

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