Search code examples
javascriptreactjsevent-handlingreact-componentreact-state-management

How to Update Component State in Each of Mapped Components When One Changes


I am making a basic app in which images are rendered to the screen. The objective is to not click the same image more than once. I'm using parent state in App comp to count the score (number of first time clicks) and local component state in Image component to see if the image has been clicked already. App Component is such:

class App extends React.Component {
  state = { score: 0 };
  array = [1, 2, 3]

  handleScoreIncrement = (e) => {
    this.setState({ score: this.state.score + 1 })
    console.log(this.state.score)
  }

  handleRestart = () => {
    this.setState({ score: 0 })
  }

  render() {

    return (
      <div>
        <Header
          score={this.state.score} />
        {this.array.map((cv, i, arr) => {
          return (
            <div key={cv}>
              <Image
                score={this.state.score}
                increaseScore={this.handleScoreIncrement}
                restart={this.handleRestart}
              />
            </div>
          )
        })
        }

      </div>
    )

  }
};


export default App; 

I passed the callbacks in the App component as props to the image component to change the state as you can see below. Just stay with me. Image Component:

class Image extends React.Component {

  state = {
    clickedAlready: false,
    score: this.props.score
  }


  handleRestart = () => {
    this.props.restart();
    if (this.props.score === 0) {
      this.setState({ clickedAlready: false })
    }
  }


  handleClickEvent = (e) => {
    if (this.state.clickedAlready) {
      alert('Clicked Already')
      this.handleRestart()
      this.setState({ clickedAlready: false })
    } else {
      this.props.increaseScore();
      this.setState({ clickedAlready: true })
    }
    console.log(this.state.score)
  }


  render() {
    return (
      <div
        style={{ border: '1px solid red', marginTop: 5 }}
        onClick={this.handleClickEvent}>Image Would Go Here
      </div>
    )
  }
}


export default Image;

NOW FOR THE PROBLEM: The problem I have is that each time I click an image, it increments correctly, but once I've pressed the same image twice, to reset the game and start over, only THAT specific image changes it's state back to {clickedAlready: false} (so as to restart the game) while the rest still say they have been clicked. It is impossible for me to restart the game if, on reset, all components other than the double-clicked one retain their previous state. I expected that my event handler in my Image component would reset the state of both the Image comp and App component, thus re-rendering both components, but maybe I am missing something. Please HELP

Essentially, I want, when one Image component has been double-clicked, for all Image components to re-set their state to {clickedAlready: false}.


Solution

  • Main things to update:

    Image

    1. Use componentDidUpdate to reset the clickedAlready state when the score prop value is updated to 0. This allows all Image components to "reset" when the score state is reset in the parent component.

      componentDidUpdate(prevProps) {
        if (prevProps.score !== this.props.score && this.props.score === 0) {
          this.setState({ clickedAlready: false });
        }
      }
      
    2. Update handleClickEvent to check the clickedAlready state to either callback to reset the game or bump the score and update the clicked status.

      handleClickEvent = (e) => {
        const { clickedAlready } = this.state;
        const { increaseScore, restart } = this.props;
        if (clickedAlready) {
          alert("Clicked Already");
          restart();
        } else {
          increaseScore();
          this.setState({ clickedAlready: true });
        }
      };
      

    Edit how-to-update-component-state-in-each-of-mapped-components-when-one-changes

    Full code in sandbox demo

    const Header = ({ score }) => <h1>Score: {score}</h1>;
    
    class Image extends React.Component {
      state = {
        clickedAlready: false
      };
    
      componentDidUpdate(prevProps) {
        if (prevProps.score !== this.props.score && this.props.score === 0) {
          this.setState({ clickedAlready: false });
        }
      }
    
      handleClickEvent = (e) => {
        const { clickedAlready } = this.state;
        const { increaseScore, restart } = this.props;
        if (clickedAlready) {
          alert("Clicked Already");
          restart();
        } else {
          increaseScore();
          this.setState({ clickedAlready: true });
        }
      };
    
      render() {
        const { clickedAlready } = this.state;
        return (
          <div
            style={{ border: "1px solid red", marginTop: 5 }}
            onClick={this.handleClickEvent}
          >
            Image Would Go Here {clickedAlready && " - clicked"}
          </div>
        );
      }
    }
    
    class App extends React.Component {
      state = { score: 0 };
      array = [1, 2, 3];
    
      componentDidUpdate() {
        const { score } = this.state;
        console.log(score);
        if (score === this.array.length) {
          alert("Won");
          this.handleRestart();
        }
      }
    
      handleScoreIncrement = (e) => {
        this.setState({ score: this.state.score + 1 });
      };
    
      handleRestart = () => {
        this.setState({ score: 0 });
      };
    
      render() {
        return (
          <div>
            <Header score={this.state.score} />
            {this.array.map((cv, i, arr) => {
              return (
                <div key={cv}>
                  <Image
                    score={this.state.score}
                    increaseScore={this.handleScoreIncrement}
                    restart={this.handleRestart}
                  />
                </div>
              );
            })}
          </div>
        );
      }
    }
    
    export default App;