HB11
HB11

Reputation: 25

Component only updates after two clicks React

I am building a React app that - among other things - generates a random number when a button is clicked and then filters an array of JSON objects to only the one at the index of that random number (i.e. JSON[random]). Normally the app is supposed to re-render after the array of JSON objects is filtered, but for some reason, on the first time the button is clicked and a random is picked, it requires two clicks to update. From then on it updates as expected, with a new random rendering each time the button is clicked.

I'm not sure if the problem is coming from App.js or somewhere lower down. On the first click, it generates a new random and supposedly saves this to state, but fails to re-render right away. On subsequent clicks, things seem to update based on the previously-generated random, while a new random is put in the queue. I would prefer the this all happens in one go: click, generate random, save to state, update to reflect the new random à la JSON[random].

This might have something to do with the way I have implemented lifecycle methods, as I'm admittedly not sure of all the nuances of each and have just tried to use whichever ones seemed to do what I wanted. If you have any suggestions there, please let me know...

Thanks!

Here are the relevant files:

App.js - where the random is generated and stored when a new click is registered in Header.state.randomClicks

class App extends Component {
  constructor(props){
    super(props)
    this.state = {headerLink: "", searchValue: "", random: 0, randomClicks: 0}

    this.generateRandom = this.generateRandom.bind(this);
  }

  getLinkFromHeader = (link) => {
    if (this.state.headerLink !== link) {
      this.setState({
        headerLink: link,
      })
    }
  }

  getSearchValueFromHeader = (string) => {
    this.setState({
      searchValue: string,
    });
  }

  getRandomMax = (max) => {
    this.setState({
      randomMax: max,
    })
  }

  getRandomClicks = (value) => {
    this.setState({
      randomClicks: value,
    })
  }

  generateRandom(number) {
    let random = Math.floor(Math.random() * number) + 1;
    console.log("generateRandom = ", random)
    return random
  }

  shouldComponentUpdate(nextProps, nextState) {
    return this.state.randomClicks !== nextState.randomClicks;
  }

  componentWillUpdate() {}

  componentDidUpdate(prevState) {
    let randomClicks = this.state.randomClicks;
    console.log("this.state.randomClicks: ", this.state.randomClicks)
    // console.log("prevState: ", prevState)
    // console.log("prevState.randomClicks = ", prevState.randomClicks)
    // ^^ is this a bug ? ^^
    let random = this.generateRandom(this.state.randomMax);
    if (this.state.random !== random) {
      this.setState({random: random})
    }
  }

  render() {
    return (
      <div className="App background">
        <div className="content">
          <Header getLinkFromHeader={this.getLinkFromHeader} getSearchValueFromHeader={this.getSearchValueFromHeader} randomClick={this.randomClick} getRandomClicks={this.getRandomClicks}/>
          <TilesContainer link={this.state.headerLink} searchValue={this.state.searchValue} getRandomMax={this.getRandomMax} random={this.state.random} randomClicks={this.state.randomClicks}/>
        </div>
      </div>
    );
  }
}

export default App

Header.js* - where the randomClick count is incremented each time RandomButton is clicked

class Header extends Component {
  constructor(props){
    super(props);
    this.state = { selectorLink: "", searchValue: "", randomClicks: 0 }

    this.randomClick = this.randomClick.bind(this);
  }


  getLinkFromSelector = (link) => {
    this.setState({
      selectorLink: link,
    })
  }

  getSearchValue = (string) => {
    this.setState({
      searchValue: string,
    })
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log("this.state !== nextState: ", this.state !== nextState)
    return this.state !== nextState;
  }

  componentDidUpdate(previousState){
    if(this.state.selectorLink !== previousState.selectorLink) {
      this.props.getLinkFromHeader(this.state.selectorLink);
    }
    this.props.getSearchValueFromHeader(this.state.searchValue);
    this.props.getRandomClicks(this.state.randomClicks);
    console.log("Header Did Update")
  }

  randomClick(){
    this.props.randomClick;
    this.setState({
      randomClicks: this.state.randomClicks += 1,
    });
  }

  render(){
    return(
      <div id="header" className="header">

        <div className="title-div">
          <div className="h1-wrapper title-wrapper">
            <h1>Pokédex Viewer App</h1>
          </div>
        </div>

        <PokedexSelector  getLinkFromSelector={this.getLinkFromSelector}/>

        <SearchBar getSearchValue={this.getSearchValue}/>

        <button type="button" id="random-button" onClick={this.randomClick}>Random Pokémon</button>
        <button type="button" id="show-all-button" onClick={this.showAllClick}>Show All</button>

      </div>
    )
  }
}

export default Header

TilesContainer.js - where the random number from App is sent and the tiles list is filtered/re-rendered

class TilesContainer extends Component {

  constructor(props){
    super(props);
    this.state = {
        pokemon: [],
        filteredPokemon: [],
        randomMax: 0,
        showDetails: false,
      };
    this.getPokemon = this.getPokemon.bind(this);
    this.tiles = this.tiles.bind(this);
    this.getPokemon(this.props.link);
  }

  getPokemon(pokedexLink) {
    let link = "";
    (pokedexLink === "")
      ? link = "https://pokeapi.co/api/v2/pokedex/national/"
      : link = this.props.link;
      fetch(link)
      .then(response => response.json())
      .then(myJson => {
        let list = myJson['pokemon_entries'];
        this.setState({
          pokemon: list,
          randomMax: list.length,
        })
        this.props.getRandomMax; // send randomMax to App
      })
  }

  filterPokemon(string) {
    if (string !== "") {
        console.log("string: ", string)
        string = string.toString().toLowerCase()
        let filteredPokemon =  this.state.pokemon.filter(pokemon => {
        const name = pokemon.pokemon_species.name;
        const nameStr = name.slice(0,string.length);
        const number = pokemon.entry_number;
        const numberStr = number.toString().slice(0, string.length);
        return (this.state.random !== 0) ? number.toString() === string : nameStr === string || numberStr === string;
      })
      if (this.props.randomClicks !== 0) { // i.e. using a random
        this.setState({
          filteredPokemon: filteredPokemon,
        })
      } else {
        this.setState({
          filteredPokemon: filteredPokemon,
          randomMax: filteredPokemon.length,
        })
      }
    } else {
      this.setState({
        filteredPokemon: [],
        randomMax: this.state.pokemon.length,
      })
    }
  }


  componentDidUpdate(prevProps, prevState) {
    if (this.props.link !== prevProps.link) {
      this.getPokemon(this.props.link)
    }
    if (this.props.searchValue !== prevProps.searchValue) {
      this.filterPokemon(this.props.searchValue)
    }
    if (this.state.randomMax !== prevState.randomMax){
      this.props.getRandomMax(this.state.randomMax);
    }
    if (this.props.random !== prevProps.random) {
      console.log("TilesContainer random: ", this.props.random)
      this.filterPokemon(this.props.random)
    }
  }

  tiles() {
    console.log("tiles() filteredPokemon: ", this.state.filteredPokemon)
    console.log("tiles() searchValue: ", this.props.searchValue)
    console.log("tiles() random: ", this.props.random)
    if (this.state.pokemon.length > 0) {
      if (this.state.filteredPokemon.length == 0 && this.props.searchValue === ""){
        return (
            this.state.pokemon.map(pokemon => (
            <Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
          ))
        )
      } else if (this.state.filteredPokemon.length > 0){
        return (
            this.state.filteredPokemon.map(pokemon => (
            <Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
          ))
        )
      }

    }
  }

  render(){
    return (
      <div id="tiles-container"
           className="tiles-container">
             {this.tiles()}
      </div>
    )
  }
}

export default TilesContainer

Upvotes: 1

Views: 1185

Answers (1)

UjinT34
UjinT34

Reputation: 4987

You should not use current state in setState and should not modify state directly. And you do no actually call this.props.randomClick and it is undefined. Change

randomClick(){
    this.props.randomClick;
    this.setState({
        randomClicks: this.state.randomClicks += 1,
    });
}

to

randomClick(){
    if (typeof(this.props.randomClick) === 'function') this.props.randomClick();
    this.setState(olState => ({
        randomClicks: olState.randomClicks + 1,
    }));
}

Also check your shouldComponentUpdate methods. They might be buggy or redundant. Looks like you prevent updating App when state.random changes. So every time you click the button you store the new random value but use the previous one. So for the initial render and for the first click you use random: 0.

And I guess that getRandomClicks should be setRandomClicks.

Upvotes: 2

Related Questions