8. Voting for a Product - Dynamically Updating the UI

  • This section will allow you to vote on your favourite products. Interacting with the application and dynamically up it!

  • You will learn how to manage interaction with your components and how to dynamically update data that is stored in a component’s state.

  • Begin by updating the product component to show its current number of votes as well as a button to click on to vote for that product.

    <div className='header'>
      <a>
        <i className='large caret up icon' />
      </a>
      {this.props.votes}
    </div>
    
  • Resulting in the following <Product> component:

    class Product extends React.Component {
      render() {
        return (
          <div className='item'>
            <div className='image'>
              <img src={this.props.productImageUrl} />
            </div>
            <div className='middle aligned content'>
              <div className='header'>
                <a>
                  <i className='large caret up icon' />
                </a>
                {this.props.votes}
              </div>
              <div className='description'>
                <a>{this.props.title}</a>
                <p>{this.props.description}</p>
              </div>
              <div className='extra'>
                <span>Submitted by:</span>
                <img className='ui avatar image' src={this.props.submitterAvatarUrl} />
              </div>
            </div>
          </div>
        );
      }
    }
    
  • Notice that this.props.votes is being accessed but is not currently being passed in by the parent <ProductRegistry>.

  • Update the <ProductRegistry> to also pass in votes as a prop:

    votes={product.votes}
    
  • Resulting in the complete <Product> definition:

    <Product
      key={'product-'+product.id}
      id={product.id}
      title={product.title}
      description={product.description}
      submitterAvatarUrl={product.submitterAvatarUrl}
      productImageUrl={product.productImageUrl}
      votes={product.votes}
    />
    

Time for some interaction!

  • When the voting caret is clicked we want to increment the product’s total vote count.

  • In order to do this we need to register the event when the given product is clicked.

  • React features many built-in listeners for such events. In fact an onClick prop exists that we can access directly.

  • Within the definition of the caret in the <Product> component add the onClick prop and create an alert whenever a click occurs.

    <div className='header'>
      <a onClick={() => alert('click')}>
        <i className='large caret up icon' />
      </a>
      {this.props.votes}
    </div>
    
  • Resulting in the following <Product> component:

    class Product extends React.Component {
      render() {
        return (
          <div className='item'>
            <div className='image'>
              <img src={this.props.productImageUrl} />
            </div>
            <div className='middle aligned content'>
              <div className='header'>
                <a onClick={() => alert('click')}>
                  <i className='large caret up icon' />
                </a>
                {this.props.votes}
              </div>
              <div className='description'>
                <a>{this.props.title}</a>
                <p>{this.props.description}</p>
              </div>
              <div className='extra'>
                <span>Submitted by:</span>
                <img className='ui avatar image' src={this.props.submitterAvatarUrl} />
              </div>
            </div>
          </div>
        );
      }
    }
    
  • Try it out!

https://raw.githubusercontent.com/Blockchain-Learning-Group/course-resources/master/product-registry-01/images/10-craet-click-alert.png
  • Now we need to update the number of votes that the clicked on product currently has every time that caret is clicked.

Note

The props of a given component are not owned by the child component itself but instead are treated as immutable, or permanent, at the child component level and owned by the parent.

So the way you currently have your components setup, parent <ProductRegistry> passing in the votes prop to child <Product> means that the <ProductRegistry> must be the one to update the given value.

Therefore, the first order of business is to have this click event on the <Product> propagated upwards to the <ProductRegistry>. React allows you to not only pass data values as props but functions as well to solve this problem!

  • Add a function within your <ProductRegistry> component to handle the event when a vote is cast:

    handleProductUpVote = (productId) => {
      console.log(productId);
    }
    
  • Pass this function to each <Product> as a new prop called onVote

    onVote={this.handleProductUpVote}
    
  • Resulting in the complete <ProductRegistry>:

    class ProductRegistry extends React.Component {
      handleProductUpVote = (productId) => {
        console.log(productId);
      }
    
      render() {
        return (
          <div className='ui unstackable items'>
            {
              Seed.products.map(product =>
                <Product
                  key={'product-'+product.id}
                  id={product.id}
                  title={product.title}
                  description={product.description}
                  submitterAvatarUrl={product.submitterAvatarUrl}
                  productImageUrl={product.productImageUrl}
                  votes={product.votes}
                  onVote={this.handleProductUpVote}
                />
              )
            }
          </div>
        );
      }
    }
    
  • Update the <Product> to no longer raise the alert but instead call its onVote prop, pass the id of the clicked component in order to determine where the event occured to cast the vote correctly:

    <a onClick={() => this.props.onVote(this.props.id)}>
    
  • Resulting in the complete <Product>:

    class Product extends React.Component {
      render() {
        return (
          <div className='item'>
            <div className='image'>
              <img src={this.props.productImageUrl} />
            </div>
            <div className='middle aligned content'>
              <div className='header'>
                <a onClick={() => this.props.onVote(this.props.id)}>
                  <i className='large caret up icon' />
                </a>
                {this.props.votes}
              </div>
              <div className='description'>
                <a>{this.props.title}</a>
                <p>{this.props.description}</p>
              </div>
              <div className='extra'>
                <span>Submitted by:</span>
                <img className='ui avatar image' src={this.props.submitterAvatarUrl} />
              </div>
            </div>
          </div>
        );
      }
    }
    
  • Try it out! Noting the id of the product logged to the browser developer console, 1,2,3 or 4, and successfully the event has been propagated upward to the parent component!

  • Complete solution may be found here

Introducing: The State!

Note

Props as we defined earlier are seen as immutable by a component and owned by a it’s parent. State is instead owned by the component itself private to that component. The state of a component is in fact mutable and accessible via a function provided by the React.Component base class called this.setState(). And it is with the call of this.setState() that the component will also no to re-render itself with the new data!

  • Begin by defining the initial state of the <ProductRegistry>:

    state = {
      products: Seed.products
    };
    
  • Update the render function to now read from the component’s state instead of the seed file directly:

  • Resulting in the complete <ProductRegistry>:

    class ProductRegistry extends React.Component {
      state = {
        products: Seed.products
      };
    
      handleProductUpVote = (productId) => {
        console.log(productId);
      }
    
      render() {
        return (
          <div className='ui unstackable items'>
            {
              this.state.products.map(product =>
                <Product
                  key={'product-'+product.id}
                  id={product.id}
                  title={product.title}
                  description={product.description}
                  submitterAvatarUrl={product.submitterAvatarUrl}
                  productImageUrl={product.productImageUrl}
                  votes={product.votes}
                  onVote={this.handleProductUpVote}
                />
              )
            }
          </div>
        );
      }
    }
    

Important

Never modify state outside of this.setState() !

State should NEVER be accessed directly, i.e. this.state = {}, outside of its initial definition.

this.setState() has very important functionality built around it that can cause odd and unexpected behaviour if avoided. Always use this.setState() when updating the state of a component.

  • Now although we noted earlier that props are seen as immutable from the given component and state mutable a slight variation to that definition must be explained

  • Yes, the state may be updated, but the current state object is said to be immutable, meaning that the state object should not be updated directly but instead replaced with a new state object

  • For example directly updating, mutating, the current state is bad practise!

    // INCORRECT!
    this.state = { products: [] };
    this.state.products.push("hello");
    
  • Instead a new state object is to be created and the state update to the new object.

    // CORRECT!
    this.state = { products: [] };
    const newProducts = this.state.products.concat("hello");
    this.setState({ products: products });
    
  • Therefore when we want to update the state when a vote has been cast we need to:

    1. Create a copy of the state
    • Map will return a copy of each item in the array it will not reference the existing.
    const nextProducts = this.state.products.map((product) => {
      return product;
    });
    
    1. Determine which product was voted for
    if (product.id === productId) {}
    
    1. Mutate the copy of the state incrementing the product’s vote count
    • Create a new product Object via Object.assign and update the votes attribute of that object to +1 of the existing product
    return Object.assign({}, product, {
      votes: product.votes + 1,
    });
    
    1. Set the state to the new object
    this.setState({ products: nextProducts });
    
  • Resulting in the following segment added within the handleProductUpVote function of the <ProductRegistry> to update the vote count of a selected product identified by its id:

    const nextProducts = this.state.products.map((product) => {
      if (product.id === productId) {
        return Object.assign({}, product, {
          votes: product.votes + 1,
        });
      } else {
        return product;
      }
    });
    
  • Resulting in the following complete <ProductRegistry>:

    class ProductRegistry extends React.Component {
      state = {
        products: Seed.products
      };
    
      handleProductUpVote = (productId) => {
        const nextProducts = this.state.products.map((product) => {
          if (product.id === productId) {
            return Object.assign({}, product, {
              votes: product.votes + 1,
            });
          } else {
            return product;
          }
        });
    
        this.setState({ products: nextProducts });
      }
    
      render() {
        return (
          <div className='ui unstackable items'>
            {
              this.state.products.map(product =>
                <Product
                  key={'product-'+product.id}
                  id={product.id}
                  title={product.title}
                  description={product.description}
                  submitterAvatarUrl={product.submitterAvatarUrl}
                  productImageUrl={product.productImageUrl}
                  votes={product.votes}
                  onVote={this.handleProductUpVote}
                />
              )
            }
          </div>
        );
      }
    }
    
  • Give it a shot!

https://raw.githubusercontent.com/Blockchain-Learning-Group/course-resources/master/product-registry-01/images/11-voting-updating-state.png

Important

All done? We recommend reviewing the complementary video series found here.