Search code examples
javascriptreactjssetstate

How to re-render a component on each of multiple state changes?


I'm still learning JS/React, so it's likely I'm doing this completely wrong. Any criticism is welcome.

I have a Canvas with a drawning on it. I want to change the color of the drawning multiple times when a button is pressed. To be clear: I want a single click on the button to change the color of the drawning multiple times.

I've tried doing this a few different ways, but they are mosly variations of two:

  • When the button is pressed, it calls the method that will change the state multiple times, but React only bothers to render the last state I set. (Which makes sense)

  • Using setTimeout for each setState, but it seems it breaks the method, and the render never changes.

Here is a sample code:

import React from 'react';

class App extends React.Component {
 constructor(props) {
      super(props);
      this.state = {
        color: "#000000",
      }
      this.changeColors = this.changeColors.bind(this);
  }
  
  changeColors() {
    let colors = ["#000000", "#0000FF", "#FF0000", "#00FF00"];
    for (let nextColor in colors) {
      console.log(`Color now ${colors[nextColor]}`);
      // This seems to break it
      //setTimeout(function(){ this.setState({color: colors[nextColor]}); }, 3000);

      // This only renders last state
      this.setState({color: colors[nextColor]});
    }
  }

  render() {
    return (
      <div className="App">
        <h1>Change Colors</h1>
        <MyButton changeColor={this.changeColors}/>
        <MyCanvas color={this.state}/>
      </div>
    );
  }
}

class MyButton extends React.Component {
  render() {
    return (
      <button 
        type="button" 
        className="btn btn-secondary" 
        onClick={() => this.props.changeColor()}>
        Color
      </button>
    );
  }
}

class MyCanvas extends React.Component {
  componentDidMount() {
      this.drawOnCanvas(this.props.color)
  }
  
  componentDidUpdate() {
      this.drawOnCanvas(this.props.color)
  }
  
  drawOnCanvas(color) {
    const ctx = this.refs.canvas.getContext('2d');
    ctx.clearRect(0, 0, 300, 300) 
    ctx.fillStyle=color.color;
    ctx.fillRect(10, 10, 100, 100);
  }
  
  render() {
    return (
      <canvas id="canvas" ref="canvas" width={300} height={300}/>
    );
  }
}

export default App;

What I'm doing wrong and how can I achieve the multiple color changes with react?


Solution

  • Without setTimeout all the renders will be basically merged into one, this is how React works. However you could try setTimeout with a dynamic timeout.

    class App extends React.Component {
     constructor(props) {
          super(props);
          this.state = {
            color: "#000000",
          }
      }
      
      changeColors = () => {
        let colors = ["#000000", "#0000FF", "#FF0000", "#00FF00"];
        colors.forEach((color, i) => {
          setTimeout(() => {
              this.setState({ color });
          }, 500 * i);
        });
      }
    
      render() {
        return (
          <div className="App" style={{ color: this.state.color }}>
            <h1>Change Colors</h1>
            <button onClick={this.changeColors}>change</button>
          </div>
        );
      }
    }
    
    ReactDOM.render(<App />, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
    <div id='root'></div>